From 497c7f0826fe83b707782cb7eeecc9cce9fd9d5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96zcan=20Seker?= Date: Wed, 19 Jul 2023 15:09:35 +0200 Subject: [PATCH 01/22] New examples, + moving old examples around --- .../.theia/launch.json | 8 + .../systemA/entities/Aantal.cm | 9 + .../systemA/entities/Klant.cm | 10 + .../systemA/entities/Order.cm | 17 ++ .../systemA/entities/Product.cm | 10 + .../AantalToProduct.relationship.cm | 8 + .../AantalToProduct0.relationship.cm | 8 + .../KlantToOrder.relationship.cm | 8 + .../KlantToOrder0.relationship.cm | 8 + .../OrderToAantal.relationship.cm | 8 + .../OrderToProduct.relationship.cm | 8 + .../systemA/source/system.diagram.cm | 272 ++++++++++++++++++ .../systemB}/entities/entityA.cm | 0 .../systemB/entities/entityC.cm | 3 + .../systemB}/entities/entityD.cm | 0 .../systemB}/entities/entityE.cm | 0 .../systemB/relationships/relationship.cm | 5 + .../systemB/views/entity.diagram.cm | 8 + .../libraries/entities/.npmrc | 0 .../libraries/entities}/entities/entityA.cm | 0 .../libraries/entities/entities/entityB.cm | 0 .../libraries/entities/entities/entityC.cm | 0 .../libraries/entities}/entities/entityD.cm | 0 .../libraries/entities/package.json | 0 .../libraries/example-library/.npmrc | 0 .../example-library/entities/entityA.cm | 3 + .../example-library/entities/entityB.cm | 0 .../example-library/entities/entityC.cm | 0 .../example-library/entities/entityD.cm | 3 + .../libraries/example-library/package.json | 0 .../relationships/aToB.relationship.cm | 0 .../relationships/cToD.relationship.cm | 0 .../example-library/views/system.diagram.cm | 0 .../libraries/relationships/.npmrc | 0 .../libraries/relationships/package.json | 0 .../relationships/aToB.relationship.cm | 0 .../relationships/cToD.relationship.cm | 0 .../libraries/system/.npmrc | 0 .../libraries/system/entities/entityE.cm | 3 + .../libraries/system/package.json | 0 .../libraries/system/views/system.diagram.cm | 0 .../registry/.verdaccio-db.json | 0 .../@crossbreeze/entities/entities-1.0.0.tgz | Bin .../@crossbreeze/entities/package.json | 0 .../example-library/example-library-1.0.0.tgz | Bin .../@crossbreeze/example-library/package.json | 0 .../@crossbreeze/relationships/package.json | 0 .../relationships/relationships-1.0.0.tgz | Bin .../registry/@crossbreeze/system/package.json | 0 .../@crossbreeze/system/system-1.0.0.tgz | Bin .../{ => verdaccio-example}/workspace/.npmrc | 0 .../workspace/.theia/settings.json | 0 .../workspace/package-lock.json | 0 .../workspace/package.json | 0 .../ws-relationships/aToD.relationship.cm | 0 .../ws-relationships/entityA.entity.cm | 0 .../workspace/ws-relationships/package.json | 0 .../workspace/ws-system/package.json | 0 .../ws-system/views/system.diagram.cm | 0 .../yaml-example/voorbeeld_taal_diagram.cm | 19 ++ .../voorbeeld_taal_entity_customer.cm | 24 ++ .../voorbeeld_taal_entity_order.cm | 21 ++ .../voorbeeld_taal_relationship.cm | 5 + package.json | 3 +- 64 files changed, 470 insertions(+), 1 deletion(-) create mode 100644 examples/cross-model-project-example/.theia/launch.json create mode 100644 examples/cross-model-project-example/systemA/entities/Aantal.cm create mode 100644 examples/cross-model-project-example/systemA/entities/Klant.cm create mode 100644 examples/cross-model-project-example/systemA/entities/Order.cm create mode 100644 examples/cross-model-project-example/systemA/entities/Product.cm create mode 100644 examples/cross-model-project-example/systemA/relationships/AantalToProduct.relationship.cm create mode 100644 examples/cross-model-project-example/systemA/relationships/AantalToProduct0.relationship.cm create mode 100644 examples/cross-model-project-example/systemA/relationships/KlantToOrder.relationship.cm create mode 100644 examples/cross-model-project-example/systemA/relationships/KlantToOrder0.relationship.cm create mode 100644 examples/cross-model-project-example/systemA/relationships/OrderToAantal.relationship.cm create mode 100644 examples/cross-model-project-example/systemA/relationships/OrderToProduct.relationship.cm create mode 100644 examples/cross-model-project-example/systemA/source/system.diagram.cm rename examples/{libraries/entities => cross-model-project-example/systemB}/entities/entityA.cm (100%) create mode 100644 examples/cross-model-project-example/systemB/entities/entityC.cm rename examples/{libraries/entities => cross-model-project-example/systemB}/entities/entityD.cm (100%) rename examples/{libraries/system => cross-model-project-example/systemB}/entities/entityE.cm (100%) create mode 100644 examples/cross-model-project-example/systemB/relationships/relationship.cm create mode 100644 examples/cross-model-project-example/systemB/views/entity.diagram.cm rename examples/{ => verdaccio-example}/libraries/entities/.npmrc (100%) rename examples/{libraries/example-library => verdaccio-example/libraries/entities}/entities/entityA.cm (100%) rename examples/{ => verdaccio-example}/libraries/entities/entities/entityB.cm (100%) rename examples/{ => verdaccio-example}/libraries/entities/entities/entityC.cm (100%) rename examples/{libraries/example-library => verdaccio-example/libraries/entities}/entities/entityD.cm (100%) rename examples/{ => verdaccio-example}/libraries/entities/package.json (100%) rename examples/{ => verdaccio-example}/libraries/example-library/.npmrc (100%) create mode 100644 examples/verdaccio-example/libraries/example-library/entities/entityA.cm rename examples/{ => verdaccio-example}/libraries/example-library/entities/entityB.cm (100%) rename examples/{ => verdaccio-example}/libraries/example-library/entities/entityC.cm (100%) create mode 100644 examples/verdaccio-example/libraries/example-library/entities/entityD.cm rename examples/{ => verdaccio-example}/libraries/example-library/package.json (100%) rename examples/{ => verdaccio-example}/libraries/example-library/relationships/aToB.relationship.cm (100%) rename examples/{ => verdaccio-example}/libraries/example-library/relationships/cToD.relationship.cm (100%) rename examples/{ => verdaccio-example}/libraries/example-library/views/system.diagram.cm (100%) rename examples/{ => verdaccio-example}/libraries/relationships/.npmrc (100%) rename examples/{ => verdaccio-example}/libraries/relationships/package.json (100%) rename examples/{ => verdaccio-example}/libraries/relationships/relationships/aToB.relationship.cm (100%) rename examples/{ => verdaccio-example}/libraries/relationships/relationships/cToD.relationship.cm (100%) rename examples/{ => verdaccio-example}/libraries/system/.npmrc (100%) create mode 100644 examples/verdaccio-example/libraries/system/entities/entityE.cm rename examples/{ => verdaccio-example}/libraries/system/package.json (100%) rename examples/{ => verdaccio-example}/libraries/system/views/system.diagram.cm (100%) rename examples/{ => verdaccio-example}/registry/.verdaccio-db.json (100%) rename examples/{ => verdaccio-example}/registry/@crossbreeze/entities/entities-1.0.0.tgz (100%) rename examples/{ => verdaccio-example}/registry/@crossbreeze/entities/package.json (100%) rename examples/{ => verdaccio-example}/registry/@crossbreeze/example-library/example-library-1.0.0.tgz (100%) rename examples/{ => verdaccio-example}/registry/@crossbreeze/example-library/package.json (100%) rename examples/{ => verdaccio-example}/registry/@crossbreeze/relationships/package.json (100%) rename examples/{ => verdaccio-example}/registry/@crossbreeze/relationships/relationships-1.0.0.tgz (100%) rename examples/{ => verdaccio-example}/registry/@crossbreeze/system/package.json (100%) rename examples/{ => verdaccio-example}/registry/@crossbreeze/system/system-1.0.0.tgz (100%) rename examples/{ => verdaccio-example}/workspace/.npmrc (100%) rename examples/{ => verdaccio-example}/workspace/.theia/settings.json (100%) rename examples/{ => verdaccio-example}/workspace/package-lock.json (100%) rename examples/{ => verdaccio-example}/workspace/package.json (100%) rename examples/{ => verdaccio-example}/workspace/ws-relationships/aToD.relationship.cm (100%) rename examples/{ => verdaccio-example}/workspace/ws-relationships/entityA.entity.cm (100%) rename examples/{ => verdaccio-example}/workspace/ws-relationships/package.json (100%) rename examples/{ => verdaccio-example}/workspace/ws-system/package.json (100%) rename examples/{ => verdaccio-example}/workspace/ws-system/views/system.diagram.cm (100%) create mode 100644 examples/yaml-example/voorbeeld_taal_diagram.cm create mode 100644 examples/yaml-example/voorbeeld_taal_entity_customer.cm create mode 100644 examples/yaml-example/voorbeeld_taal_entity_order.cm create mode 100644 examples/yaml-example/voorbeeld_taal_relationship.cm diff --git a/examples/cross-model-project-example/.theia/launch.json b/examples/cross-model-project-example/.theia/launch.json new file mode 100644 index 00000000..7e4253b3 --- /dev/null +++ b/examples/cross-model-project-example/.theia/launch.json @@ -0,0 +1,8 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + "version": "0.2.0", + "configurations": [ + + ] +} diff --git a/examples/cross-model-project-example/systemA/entities/Aantal.cm b/examples/cross-model-project-example/systemA/entities/Aantal.cm new file mode 100644 index 00000000..1561bfaf --- /dev/null +++ b/examples/cross-model-project-example/systemA/entities/Aantal.cm @@ -0,0 +1,9 @@ +entity Aantal { + description := "Test description"; + attributes { + nope := 'Float'; + test123456 := 'Bool'; + test3 := 'Float'; + test4 := 'Char'; + } +} \ No newline at end of file diff --git a/examples/cross-model-project-example/systemA/entities/Klant.cm b/examples/cross-model-project-example/systemA/entities/Klant.cm new file mode 100644 index 00000000..d14bfb1e --- /dev/null +++ b/examples/cross-model-project-example/systemA/entities/Klant.cm @@ -0,0 +1,10 @@ +entity Klant { + description := "test123"; + attributes { + werk := 'Float'; + test49999912somsdit := 'Char'; + wat111234 := 'Varchar'; + hall1123 := 'Integer'; + ditwerktdusnietaltijd := 'test1234'; + } +} \ No newline at end of file diff --git a/examples/cross-model-project-example/systemA/entities/Order.cm b/examples/cross-model-project-example/systemA/entities/Order.cm new file mode 100644 index 00000000..7f07dce4 --- /dev/null +++ b/examples/cross-model-project-example/systemA/entities/Order.cm @@ -0,0 +1,17 @@ +entity Order { + description := "Orders geplaatst door de klant"; + attributes { + test49999912somsdit := 'Varchar'; + werk := 'Integer'; + wat111234 := 'Varchar'; + hall1123 := 'Integer'; + ditwerktdusnietaltijd := 'test1234'; + empty_attribute5 := 'Float'; + empty_attribute6 := 'Float'; + empty_attribute7 := 'Float'; + empty_attribute8 := 'Float'; + empty_attribute9 := 'Float'; + empty_attribute10 := 'Float'; + empty_attribute11 := 'Float'; + } +} \ No newline at end of file diff --git a/examples/cross-model-project-example/systemA/entities/Product.cm b/examples/cross-model-project-example/systemA/entities/Product.cm new file mode 100644 index 00000000..a402eb76 --- /dev/null +++ b/examples/cross-model-project-example/systemA/entities/Product.cm @@ -0,0 +1,10 @@ +entity Product { + description := "Producten die verkocht worden"; + attributes { + test1 := 'test2'; + test2 := 'test3'; + test3 := 'test'; + test4 := 'test'; + test5 := 'test'; + } +} \ No newline at end of file diff --git a/examples/cross-model-project-example/systemA/relationships/AantalToProduct.relationship.cm b/examples/cross-model-project-example/systemA/relationships/AantalToProduct.relationship.cm new file mode 100644 index 00000000..eaa57c1d --- /dev/null +++ b/examples/cross-model-project-example/systemA/relationships/AantalToProduct.relationship.cm @@ -0,0 +1,8 @@ +relationship AantalToProduct { + source := Aantal; + target := Product ; + type := 1:1; + properties { + + } + } \ No newline at end of file diff --git a/examples/cross-model-project-example/systemA/relationships/AantalToProduct0.relationship.cm b/examples/cross-model-project-example/systemA/relationships/AantalToProduct0.relationship.cm new file mode 100644 index 00000000..97b6d9fc --- /dev/null +++ b/examples/cross-model-project-example/systemA/relationships/AantalToProduct0.relationship.cm @@ -0,0 +1,8 @@ +relationship AantalToProduct0 { + source := Aantal; + target := Product; + type := 1:1; + properties { + + } + } \ No newline at end of file diff --git a/examples/cross-model-project-example/systemA/relationships/KlantToOrder.relationship.cm b/examples/cross-model-project-example/systemA/relationships/KlantToOrder.relationship.cm new file mode 100644 index 00000000..307bc6af --- /dev/null +++ b/examples/cross-model-project-example/systemA/relationships/KlantToOrder.relationship.cm @@ -0,0 +1,8 @@ +relationship KlantToOrder { + source := Klant; + target := Order; + type := 1:1; + properties { + + } + } \ No newline at end of file diff --git a/examples/cross-model-project-example/systemA/relationships/KlantToOrder0.relationship.cm b/examples/cross-model-project-example/systemA/relationships/KlantToOrder0.relationship.cm new file mode 100644 index 00000000..589c7d2a --- /dev/null +++ b/examples/cross-model-project-example/systemA/relationships/KlantToOrder0.relationship.cm @@ -0,0 +1,8 @@ +relationship KlantToOrder0 { + source := Klant; + target := Order; + type := 1:1; + properties { + + } + } \ No newline at end of file diff --git a/examples/cross-model-project-example/systemA/relationships/OrderToAantal.relationship.cm b/examples/cross-model-project-example/systemA/relationships/OrderToAantal.relationship.cm new file mode 100644 index 00000000..2ef7e052 --- /dev/null +++ b/examples/cross-model-project-example/systemA/relationships/OrderToAantal.relationship.cm @@ -0,0 +1,8 @@ +relationship OrderToAantal { + source := Order; + target := Aantal; + type := 1:1; + properties { + + } + } \ No newline at end of file diff --git a/examples/cross-model-project-example/systemA/relationships/OrderToProduct.relationship.cm b/examples/cross-model-project-example/systemA/relationships/OrderToProduct.relationship.cm new file mode 100644 index 00000000..fe7145a8 --- /dev/null +++ b/examples/cross-model-project-example/systemA/relationships/OrderToProduct.relationship.cm @@ -0,0 +1,8 @@ +relationship OrderToProduct { + source := Order; + target := Product; + type := 1:1; + properties { + + } + } \ No newline at end of file diff --git a/examples/cross-model-project-example/systemA/source/system.diagram.cm b/examples/cross-model-project-example/systemA/source/system.diagram.cm new file mode 100644 index 00000000..cab742d8 --- /dev/null +++ b/examples/cross-model-project-example/systemA/source/system.diagram.cm @@ -0,0 +1,272 @@ +diagram { + node KlantN for Klant { + x := 227.55764999671163; + y := 23.42315729156587; + width := 171.0640411376953; + height := 132; + }; +node ProductN for Product { + x := 751.1084098192446; + y := 400.43789846410783; + width := 113.9998640556468; + height := 141.06518762453462; + }; +node AantalN for Aantal { + x := 720.0619968875156; + y := -38.00518218292889; + width := 133.3416748046875; + height := 170; + }; +node OrderN for Order { + x := 461.019229870532; + y := 22.455090022486672; + width := 171.0640411376953; + height := 132; + }; +node AantalNode for unknown/Aantal { + x := 7.17229932708031; + y := 159.86311597766826; + width := 132.084401845932; + height := 170; + }; +node KlantNode for unknown/Klant { + x := -88.19829508665896; + y := -75.9252393169105; + width := 170.21827483177185; + height := 132; + }; +node CNode for C { + x := -303.54256511586857; + y := 83.24109552114436; + width := 178.2309737524657; + height := 97.48529625107477; + }; +node ProductNode for Product { + x := 4.6235348891893295; + y := 473.5848221942176; + width := 10; + height := 10; + }; +node ProductNode1 for Product { + x := 298.6653553106821; + y := 549.3423217787945; + width := 83.36386108398438; + height := 132; + }; +node ProductNode12 for Product { + x := 175.3989153086589; + y := 401.6793988597042; + width := 10; + height := 10; + }; +node ProductNode123 for Product { + x := 84.23311072382927; + y := 664.9046092806911; + width := 10; + height := 10; + }; +node ProductNode1234 for Product { + x := 285.8251011438048; + y := 772.7627442824614; + width := 83.36386108398438; + height := 132; + }; +node ProductNode12345 for Product { + x := 533.6420065645389; + y := 777.8988459492124; + width := 83.36386108398438; + height := 132; + }; +node AantalNode1 for unknown/Aantal { + x := 1330.749586994124; + y := 559.3423217787945; + width := 10; + height := 10; + }; +node AantalNode12 for unknown/Aantal { + x := 1715.9572120004464; + y := 633.8157959466835; + width := 10; + height := 10; + }; +node AantalNode123 for unknown/Aantal { + x := 1332.0336124108117; + y := 222.9276626066063; + width := 133.3416748046875; + height := 170; + }; +node AantalNode1234 for unknown/Aantal { + x := -305.09879386605843; + y := 247.32414552367325; + width := 133.3416748046875; + height := 170; + }; +node AantalNode12345 for unknown/Aantal { + x := -317.93904803293583; + y := 491.28897469434423; + width := 133.3416748046875; + height := 170; + }; +node ENode for unknown/E { + x := -427.9793782188551; + y := 31.841103463085908; + width := 10; + height := 10; + }; +node ENode1 for unknown/E { + x := -440.8196323857325; + y := 528.7589397212417; + width := 10; + height := 10; + }; +node ENode2 for unknown/E { + x := -499.8848015533686; + y := 550.5873718049334; + width := 10; + height := 10; + }; +node DNode for unknown/D { + x := -524.2812844704357; + y := 503.0784313874869; + width := 10; + height := 10; + }; +node ENode3 for unknown/E { + x := -452.3758611359222; + y := 610.9365663892571; + width := 10; + height := 10; + }; +node ENode4 for unknown/E { + x := -430.5474290522306; + y := 691.8301676405848; + width := 10; + height := 10; + }; +node ENode5 for unknown/E { + x := -352.22187863427837; + y := 702.1023709740867; + width := 10; + height := 10; + }; +node ENode6 for unknown/E { + x := -393.3106919682861; + y := 802.2563534757305; + width := 10; + height := 10; + }; +node ENode7 for unknown/E { + x := -288.0206077998913; + y := 804.8244043091061; + width := 10; + height := 10; + }; +node KlantNode1 for unknown/Klant { + x := 407.3538168651514; + y := 1188.0036571285125; + width := 10; + height := 10; + }; +node KlantNode2 for unknown/Klant { + x := 785.6515846658832; + y := 1170.5437601530941; + width := 10; + height := 10; + }; +node AantalNode2 for Aantal { + x := 573.4077780339535; + y := 1006.314670203565; + width := 10; + height := 10; + }; +node ANode for A { + x := 273.67954662260445; + y := 1058.6943611298202; + width := 10; + height := 10; + }; +node CNode1 for C { + x := -91.52329844656362; + y := 1112.5290434706935; + width := 10; + height := 10; + }; +node ANode1 for unknown/A { + x := 127.9954652584571; + y := 1398.9774122481513; + width := 29; + height := 32; + }; +node ENode8 for unknown/E { + x := 130.7205081853661; + y := 1273.6632106886589; + width := 28; + height := 32; + }; +node DNode1 for unknown/D { + x := 191.6452076970031; + y := 1305.4880819079317; + width := 28; + height := 32; + }; +node CNode2 for unknown/C { + x := 307.8595809641316; + y := 1350.4078758587686; + width := 28; + height := 32; + }; +node AantalNode3 for unknown/Aantal { + x := 12.829215051195275; + y := -319.56846840292644; + width := 10; + height := 10; + }; +node KlantNode3 for unknown/Klant { + x := 708.9582229066704; + y := -313.40803470509036; + width := 10; + height := 10; + }; +node AantalNode4 for unknown/Aantal { + x := -193.54531382631282; + y := -190.19936074836912; + width := 10; + height := 10; + }; +node AantalNode5 for unknown/Aantal { + x := -212.026614919821; + y := -224.08174608646746; + width := 10; + height := 10; + }; +node KlantNode4 for unknown/Klant { + x := -292.1122529916898; + y := -177.878493352697; + width := 10; + height := 10; + }; +node AantalNode6 for unknown/Aantal { + x := 2501.923662167497; + y := -252.2931382342814; + width := 133.3416748046875; + height := 170; + }; +node KlantNode5 for unknown/Klant { + x := 2864.363377909348; + y := -226.13769483023026; + width := 10; + height := 10; + }; + edge KlantToOrder for KlantToOrder { + source := KlantN; + target := OrderN; + }; +edge OrderToAantal for OrderToAantal { + source := OrderN; + target := AantalN; + }; +edge AantalToProduct0 for unknown/AantalToProduct0 { + source := AantalN; + target := ProductN; + }; + } \ No newline at end of file diff --git a/examples/libraries/entities/entities/entityA.cm b/examples/cross-model-project-example/systemB/entities/entityA.cm similarity index 100% rename from examples/libraries/entities/entities/entityA.cm rename to examples/cross-model-project-example/systemB/entities/entityA.cm diff --git a/examples/cross-model-project-example/systemB/entities/entityC.cm b/examples/cross-model-project-example/systemB/entities/entityC.cm new file mode 100644 index 00000000..a6c63580 --- /dev/null +++ b/examples/cross-model-project-example/systemB/entities/entityC.cm @@ -0,0 +1,3 @@ +entity C { + description := "hello"; +} diff --git a/examples/libraries/entities/entities/entityD.cm b/examples/cross-model-project-example/systemB/entities/entityD.cm similarity index 100% rename from examples/libraries/entities/entities/entityD.cm rename to examples/cross-model-project-example/systemB/entities/entityD.cm diff --git a/examples/libraries/system/entities/entityE.cm b/examples/cross-model-project-example/systemB/entities/entityE.cm similarity index 100% rename from examples/libraries/system/entities/entityE.cm rename to examples/cross-model-project-example/systemB/entities/entityE.cm diff --git a/examples/cross-model-project-example/systemB/relationships/relationship.cm b/examples/cross-model-project-example/systemB/relationships/relationship.cm new file mode 100644 index 00000000..1a82d0fa --- /dev/null +++ b/examples/cross-model-project-example/systemB/relationships/relationship.cm @@ -0,0 +1,5 @@ +relationship aToB { + source := A; + target := C; + type := n:m; +} \ No newline at end of file diff --git a/examples/cross-model-project-example/systemB/views/entity.diagram.cm b/examples/cross-model-project-example/systemB/views/entity.diagram.cm new file mode 100644 index 00000000..cbf585d2 --- /dev/null +++ b/examples/cross-model-project-example/systemB/views/entity.diagram.cm @@ -0,0 +1,8 @@ +diagram { + node mynode for A { + x := 0; + y := 0; + width := 100; + height := 100; + }; +} \ No newline at end of file diff --git a/examples/libraries/entities/.npmrc b/examples/verdaccio-example/libraries/entities/.npmrc similarity index 100% rename from examples/libraries/entities/.npmrc rename to examples/verdaccio-example/libraries/entities/.npmrc diff --git a/examples/libraries/example-library/entities/entityA.cm b/examples/verdaccio-example/libraries/entities/entities/entityA.cm similarity index 100% rename from examples/libraries/example-library/entities/entityA.cm rename to examples/verdaccio-example/libraries/entities/entities/entityA.cm diff --git a/examples/libraries/entities/entities/entityB.cm b/examples/verdaccio-example/libraries/entities/entities/entityB.cm similarity index 100% rename from examples/libraries/entities/entities/entityB.cm rename to examples/verdaccio-example/libraries/entities/entities/entityB.cm diff --git a/examples/libraries/entities/entities/entityC.cm b/examples/verdaccio-example/libraries/entities/entities/entityC.cm similarity index 100% rename from examples/libraries/entities/entities/entityC.cm rename to examples/verdaccio-example/libraries/entities/entities/entityC.cm diff --git a/examples/libraries/example-library/entities/entityD.cm b/examples/verdaccio-example/libraries/entities/entities/entityD.cm similarity index 100% rename from examples/libraries/example-library/entities/entityD.cm rename to examples/verdaccio-example/libraries/entities/entities/entityD.cm diff --git a/examples/libraries/entities/package.json b/examples/verdaccio-example/libraries/entities/package.json similarity index 100% rename from examples/libraries/entities/package.json rename to examples/verdaccio-example/libraries/entities/package.json diff --git a/examples/libraries/example-library/.npmrc b/examples/verdaccio-example/libraries/example-library/.npmrc similarity index 100% rename from examples/libraries/example-library/.npmrc rename to examples/verdaccio-example/libraries/example-library/.npmrc diff --git a/examples/verdaccio-example/libraries/example-library/entities/entityA.cm b/examples/verdaccio-example/libraries/example-library/entities/entityA.cm new file mode 100644 index 00000000..03b895bd --- /dev/null +++ b/examples/verdaccio-example/libraries/example-library/entities/entityA.cm @@ -0,0 +1,3 @@ +entity A { + description := "hello"; +} diff --git a/examples/libraries/example-library/entities/entityB.cm b/examples/verdaccio-example/libraries/example-library/entities/entityB.cm similarity index 100% rename from examples/libraries/example-library/entities/entityB.cm rename to examples/verdaccio-example/libraries/example-library/entities/entityB.cm diff --git a/examples/libraries/example-library/entities/entityC.cm b/examples/verdaccio-example/libraries/example-library/entities/entityC.cm similarity index 100% rename from examples/libraries/example-library/entities/entityC.cm rename to examples/verdaccio-example/libraries/example-library/entities/entityC.cm diff --git a/examples/verdaccio-example/libraries/example-library/entities/entityD.cm b/examples/verdaccio-example/libraries/example-library/entities/entityD.cm new file mode 100644 index 00000000..4604c447 --- /dev/null +++ b/examples/verdaccio-example/libraries/example-library/entities/entityD.cm @@ -0,0 +1,3 @@ +entity D { + description := "hello"; +} diff --git a/examples/libraries/example-library/package.json b/examples/verdaccio-example/libraries/example-library/package.json similarity index 100% rename from examples/libraries/example-library/package.json rename to examples/verdaccio-example/libraries/example-library/package.json diff --git a/examples/libraries/example-library/relationships/aToB.relationship.cm b/examples/verdaccio-example/libraries/example-library/relationships/aToB.relationship.cm similarity index 100% rename from examples/libraries/example-library/relationships/aToB.relationship.cm rename to examples/verdaccio-example/libraries/example-library/relationships/aToB.relationship.cm diff --git a/examples/libraries/example-library/relationships/cToD.relationship.cm b/examples/verdaccio-example/libraries/example-library/relationships/cToD.relationship.cm similarity index 100% rename from examples/libraries/example-library/relationships/cToD.relationship.cm rename to examples/verdaccio-example/libraries/example-library/relationships/cToD.relationship.cm diff --git a/examples/libraries/example-library/views/system.diagram.cm b/examples/verdaccio-example/libraries/example-library/views/system.diagram.cm similarity index 100% rename from examples/libraries/example-library/views/system.diagram.cm rename to examples/verdaccio-example/libraries/example-library/views/system.diagram.cm diff --git a/examples/libraries/relationships/.npmrc b/examples/verdaccio-example/libraries/relationships/.npmrc similarity index 100% rename from examples/libraries/relationships/.npmrc rename to examples/verdaccio-example/libraries/relationships/.npmrc diff --git a/examples/libraries/relationships/package.json b/examples/verdaccio-example/libraries/relationships/package.json similarity index 100% rename from examples/libraries/relationships/package.json rename to examples/verdaccio-example/libraries/relationships/package.json diff --git a/examples/libraries/relationships/relationships/aToB.relationship.cm b/examples/verdaccio-example/libraries/relationships/relationships/aToB.relationship.cm similarity index 100% rename from examples/libraries/relationships/relationships/aToB.relationship.cm rename to examples/verdaccio-example/libraries/relationships/relationships/aToB.relationship.cm diff --git a/examples/libraries/relationships/relationships/cToD.relationship.cm b/examples/verdaccio-example/libraries/relationships/relationships/cToD.relationship.cm similarity index 100% rename from examples/libraries/relationships/relationships/cToD.relationship.cm rename to examples/verdaccio-example/libraries/relationships/relationships/cToD.relationship.cm diff --git a/examples/libraries/system/.npmrc b/examples/verdaccio-example/libraries/system/.npmrc similarity index 100% rename from examples/libraries/system/.npmrc rename to examples/verdaccio-example/libraries/system/.npmrc diff --git a/examples/verdaccio-example/libraries/system/entities/entityE.cm b/examples/verdaccio-example/libraries/system/entities/entityE.cm new file mode 100644 index 00000000..06f2bfa6 --- /dev/null +++ b/examples/verdaccio-example/libraries/system/entities/entityE.cm @@ -0,0 +1,3 @@ +entity E { + description := "hello"; +} diff --git a/examples/libraries/system/package.json b/examples/verdaccio-example/libraries/system/package.json similarity index 100% rename from examples/libraries/system/package.json rename to examples/verdaccio-example/libraries/system/package.json diff --git a/examples/libraries/system/views/system.diagram.cm b/examples/verdaccio-example/libraries/system/views/system.diagram.cm similarity index 100% rename from examples/libraries/system/views/system.diagram.cm rename to examples/verdaccio-example/libraries/system/views/system.diagram.cm diff --git a/examples/registry/.verdaccio-db.json b/examples/verdaccio-example/registry/.verdaccio-db.json similarity index 100% rename from examples/registry/.verdaccio-db.json rename to examples/verdaccio-example/registry/.verdaccio-db.json diff --git a/examples/registry/@crossbreeze/entities/entities-1.0.0.tgz b/examples/verdaccio-example/registry/@crossbreeze/entities/entities-1.0.0.tgz similarity index 100% rename from examples/registry/@crossbreeze/entities/entities-1.0.0.tgz rename to examples/verdaccio-example/registry/@crossbreeze/entities/entities-1.0.0.tgz diff --git a/examples/registry/@crossbreeze/entities/package.json b/examples/verdaccio-example/registry/@crossbreeze/entities/package.json similarity index 100% rename from examples/registry/@crossbreeze/entities/package.json rename to examples/verdaccio-example/registry/@crossbreeze/entities/package.json diff --git a/examples/registry/@crossbreeze/example-library/example-library-1.0.0.tgz b/examples/verdaccio-example/registry/@crossbreeze/example-library/example-library-1.0.0.tgz similarity index 100% rename from examples/registry/@crossbreeze/example-library/example-library-1.0.0.tgz rename to examples/verdaccio-example/registry/@crossbreeze/example-library/example-library-1.0.0.tgz diff --git a/examples/registry/@crossbreeze/example-library/package.json b/examples/verdaccio-example/registry/@crossbreeze/example-library/package.json similarity index 100% rename from examples/registry/@crossbreeze/example-library/package.json rename to examples/verdaccio-example/registry/@crossbreeze/example-library/package.json diff --git a/examples/registry/@crossbreeze/relationships/package.json b/examples/verdaccio-example/registry/@crossbreeze/relationships/package.json similarity index 100% rename from examples/registry/@crossbreeze/relationships/package.json rename to examples/verdaccio-example/registry/@crossbreeze/relationships/package.json diff --git a/examples/registry/@crossbreeze/relationships/relationships-1.0.0.tgz b/examples/verdaccio-example/registry/@crossbreeze/relationships/relationships-1.0.0.tgz similarity index 100% rename from examples/registry/@crossbreeze/relationships/relationships-1.0.0.tgz rename to examples/verdaccio-example/registry/@crossbreeze/relationships/relationships-1.0.0.tgz diff --git a/examples/registry/@crossbreeze/system/package.json b/examples/verdaccio-example/registry/@crossbreeze/system/package.json similarity index 100% rename from examples/registry/@crossbreeze/system/package.json rename to examples/verdaccio-example/registry/@crossbreeze/system/package.json diff --git a/examples/registry/@crossbreeze/system/system-1.0.0.tgz b/examples/verdaccio-example/registry/@crossbreeze/system/system-1.0.0.tgz similarity index 100% rename from examples/registry/@crossbreeze/system/system-1.0.0.tgz rename to examples/verdaccio-example/registry/@crossbreeze/system/system-1.0.0.tgz diff --git a/examples/workspace/.npmrc b/examples/verdaccio-example/workspace/.npmrc similarity index 100% rename from examples/workspace/.npmrc rename to examples/verdaccio-example/workspace/.npmrc diff --git a/examples/workspace/.theia/settings.json b/examples/verdaccio-example/workspace/.theia/settings.json similarity index 100% rename from examples/workspace/.theia/settings.json rename to examples/verdaccio-example/workspace/.theia/settings.json diff --git a/examples/workspace/package-lock.json b/examples/verdaccio-example/workspace/package-lock.json similarity index 100% rename from examples/workspace/package-lock.json rename to examples/verdaccio-example/workspace/package-lock.json diff --git a/examples/workspace/package.json b/examples/verdaccio-example/workspace/package.json similarity index 100% rename from examples/workspace/package.json rename to examples/verdaccio-example/workspace/package.json diff --git a/examples/workspace/ws-relationships/aToD.relationship.cm b/examples/verdaccio-example/workspace/ws-relationships/aToD.relationship.cm similarity index 100% rename from examples/workspace/ws-relationships/aToD.relationship.cm rename to examples/verdaccio-example/workspace/ws-relationships/aToD.relationship.cm diff --git a/examples/workspace/ws-relationships/entityA.entity.cm b/examples/verdaccio-example/workspace/ws-relationships/entityA.entity.cm similarity index 100% rename from examples/workspace/ws-relationships/entityA.entity.cm rename to examples/verdaccio-example/workspace/ws-relationships/entityA.entity.cm diff --git a/examples/workspace/ws-relationships/package.json b/examples/verdaccio-example/workspace/ws-relationships/package.json similarity index 100% rename from examples/workspace/ws-relationships/package.json rename to examples/verdaccio-example/workspace/ws-relationships/package.json diff --git a/examples/workspace/ws-system/package.json b/examples/verdaccio-example/workspace/ws-system/package.json similarity index 100% rename from examples/workspace/ws-system/package.json rename to examples/verdaccio-example/workspace/ws-system/package.json diff --git a/examples/workspace/ws-system/views/system.diagram.cm b/examples/verdaccio-example/workspace/ws-system/views/system.diagram.cm similarity index 100% rename from examples/workspace/ws-system/views/system.diagram.cm rename to examples/verdaccio-example/workspace/ws-system/views/system.diagram.cm diff --git a/examples/yaml-example/voorbeeld_taal_diagram.cm b/examples/yaml-example/voorbeeld_taal_diagram.cm new file mode 100644 index 00000000..3c30270e --- /dev/null +++ b/examples/yaml-example/voorbeeld_taal_diagram.cm @@ -0,0 +1,19 @@ +diagram: + id: "Systemdiagram1" + nodes: + - id: 'CustomerNode' + for: 'Customer' + x: 100 + y: 100 + height: 100 + width: 100 + - id: 'OrderNode' + for: 'Order' + x: 100 + y: 100 + height: 100 + width: 100 + edges: + - id: 'OrderCustomerEdge' + for: 'Order_Customer' + diff --git a/examples/yaml-example/voorbeeld_taal_entity_customer.cm b/examples/yaml-example/voorbeeld_taal_entity_customer.cm new file mode 100644 index 00000000..5aabeedb --- /dev/null +++ b/examples/yaml-example/voorbeeld_taal_entity_customer.cm @@ -0,0 +1,24 @@ +entity: + id: 'Customer' + name: 'Customer' + description: 'A customer with whom a transaction has been made.' + attributes: + - id: 'Id' + name: 'Id' + datatype: 'int' + - id: 'FirstName' + name: 'FirstName' + datatype: 'varchar' + - id: 'LastName' + name: 'LastName' + datatype: 'varchar' + - id: 'City' + name: 'City' + datatype: 'varchar' + - id: 'Country' + name: 'Country' + datatype: 'varchar' + - id: 'Phone' + name: 'Phone' + datatype: 'varchar' + diff --git a/examples/yaml-example/voorbeeld_taal_entity_order.cm b/examples/yaml-example/voorbeeld_taal_entity_order.cm new file mode 100644 index 00000000..2496538f --- /dev/null +++ b/examples/yaml-example/voorbeeld_taal_entity_order.cm @@ -0,0 +1,21 @@ +entity: + id: 'Order' + attributes: + - id : 'Id' + name: 'Id' + datatype: 'int' + - id: 'OrderDate' + name: 'OrderDate' + datatype: 'datetime' + - id: 'OrderNumber' + name: 'OrderNumber' + datatype: 'varchar' + - id: 'CustomerId' + name: 'CustomerId' + datatype: 'int' + - id: 'TotalAmount' + name: 'TotalAmount' + datatype: 'Float' + name: 'Customer orders' + description: 'Order placed by a customer in the Customer table.' + \ No newline at end of file diff --git a/examples/yaml-example/voorbeeld_taal_relationship.cm b/examples/yaml-example/voorbeeld_taal_relationship.cm new file mode 100644 index 00000000..e2731216 --- /dev/null +++ b/examples/yaml-example/voorbeeld_taal_relationship.cm @@ -0,0 +1,5 @@ +relationship: + id: 'Order_Customer' + parent: 'Customer' + child: 'Order' + type: 1:1 \ No newline at end of file diff --git a/package.json b/package.json index d615f90a..9ac73d1e 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "test": "vitest --config configs/vitest.config.ts", "theia:browser": "yarn --cwd applications/browser-app", "theia:electron": "yarn --cwd applications/electron-app", - "watch": "lerna run --parallel watch" + "watch": "lerna run --parallel watch", + "clean": "lerna run clean && rimraf node_modules" }, "devDependencies": { "@testing-library/react": "^11.2.7", From 62ae32c295b350f78a479fddc172de13ef41986d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96zcan=20Seker?= Date: Wed, 16 Aug 2023 16:43:42 +0200 Subject: [PATCH 02/22] Does not work good enough yet, but it work enough to keep going --- ...m.cm => voorbeeld_taal_diagram.diagram.cm} | 2 + .../voorbeeld_taal_entity_customer.cm | 3 +- .../voorbeeld_taal_entity_order.cm | 6 +- ...orbeeld_taal_relationship.relationship.cm} | 0 .../language-configuration.json | 4 +- extensions/crossmodel-lang/package.json | 2 +- .../handler/add-entity-operation-handler.ts | 56 +- .../handler/create-edge-operation-handler.ts | 131 +- .../handler/drop-entity-operation-handler.ts | 60 +- .../model/builders/gentity-node.ts | 114 +- .../model/cross-model-gmodel-factory.ts | 64 +- .../glsp-server/model/cross-model-state.ts | 82 +- .../language-server/cross-model-formatter.ts | 37 +- .../src/language-server/cross-model-module.ts | 186 ++- .../language-server/cross-model-serializer.ts | 188 +-- .../language-server/cross-model-validator.ts | 63 +- .../src/language-server/cross-model.langium | 159 +- .../src/language-server/generated/ast.ts | 101 +- .../src/language-server/generated/grammar.ts | 1442 +++++++++++------ .../lexer/cross-model-indent-stack.ts | 81 + .../lexer/cross-model-indentation-tokens.ts | 140 ++ .../lexer/cross-model-lexer-error.ts | 14 + .../lexer/cross-model-lexer.ts | 83 + .../lexer/cross-model-token-generator.ts | 84 + .../syntaxes/cross-model.tmLanguage.json | 31 +- 25 files changed, 1964 insertions(+), 1169 deletions(-) rename examples/yaml-example/{voorbeeld_taal_diagram.cm => voorbeeld_taal_diagram.diagram.cm} (80%) rename examples/yaml-example/{voorbeeld_taal_relationship.cm => voorbeeld_taal_relationship.relationship.cm} (100%) create mode 100644 extensions/crossmodel-lang/src/language-server/lexer/cross-model-indent-stack.ts create mode 100644 extensions/crossmodel-lang/src/language-server/lexer/cross-model-indentation-tokens.ts create mode 100644 extensions/crossmodel-lang/src/language-server/lexer/cross-model-lexer-error.ts create mode 100644 extensions/crossmodel-lang/src/language-server/lexer/cross-model-lexer.ts create mode 100644 extensions/crossmodel-lang/src/language-server/lexer/cross-model-token-generator.ts diff --git a/examples/yaml-example/voorbeeld_taal_diagram.cm b/examples/yaml-example/voorbeeld_taal_diagram.diagram.cm similarity index 80% rename from examples/yaml-example/voorbeeld_taal_diagram.cm rename to examples/yaml-example/voorbeeld_taal_diagram.diagram.cm index 3c30270e..c5cb9c93 100644 --- a/examples/yaml-example/voorbeeld_taal_diagram.cm +++ b/examples/yaml-example/voorbeeld_taal_diagram.diagram.cm @@ -16,4 +16,6 @@ diagram: edges: - id: 'OrderCustomerEdge' for: 'Order_Customer' + description: "This is also a test" + description: "This should be the description" diff --git a/examples/yaml-example/voorbeeld_taal_entity_customer.cm b/examples/yaml-example/voorbeeld_taal_entity_customer.cm index 5aabeedb..7ecd5329 100644 --- a/examples/yaml-example/voorbeeld_taal_entity_customer.cm +++ b/examples/yaml-example/voorbeeld_taal_entity_customer.cm @@ -20,5 +20,4 @@ entity: datatype: 'varchar' - id: 'Phone' name: 'Phone' - datatype: 'varchar' - + datatype: 'varchar' \ No newline at end of file diff --git a/examples/yaml-example/voorbeeld_taal_entity_order.cm b/examples/yaml-example/voorbeeld_taal_entity_order.cm index 2496538f..5444895b 100644 --- a/examples/yaml-example/voorbeeld_taal_entity_order.cm +++ b/examples/yaml-example/voorbeeld_taal_entity_order.cm @@ -1,5 +1,6 @@ entity: id: 'Order' + description: 'Order placed by a customer in the Customer table.' attributes: - id : 'Id' name: 'Id' @@ -15,7 +16,4 @@ entity: datatype: 'int' - id: 'TotalAmount' name: 'TotalAmount' - datatype: 'Float' - name: 'Customer orders' - description: 'Order placed by a customer in the Customer table.' - \ No newline at end of file + datatype: 'Float' \ No newline at end of file diff --git a/examples/yaml-example/voorbeeld_taal_relationship.cm b/examples/yaml-example/voorbeeld_taal_relationship.relationship.cm similarity index 100% rename from examples/yaml-example/voorbeeld_taal_relationship.cm rename to examples/yaml-example/voorbeeld_taal_relationship.relationship.cm diff --git a/extensions/crossmodel-lang/language-configuration.json b/extensions/crossmodel-lang/language-configuration.json index 8f162a0c..e24db2d7 100644 --- a/extensions/crossmodel-lang/language-configuration.json +++ b/extensions/crossmodel-lang/language-configuration.json @@ -1,9 +1,7 @@ { "comments": { // symbol used for single line comment. Remove this entry if your language does not support line comments - "lineComment": "//", - // symbols used for start and end a block comment. Remove this entry if your language does not support block comments - "blockComment": [ "/*", "*/" ] + "lineComment": "#" }, // symbols used as brackets "brackets": [ diff --git a/extensions/crossmodel-lang/package.json b/extensions/crossmodel-lang/package.json index 599bfb95..38e7e1a0 100644 --- a/extensions/crossmodel-lang/package.json +++ b/extensions/crossmodel-lang/package.json @@ -86,7 +86,7 @@ ] }, "activationEvents": [ - "*" + "onLanguage:cross-model" ], "dependencies": { "@crossbreeze/protocol": "0.0.0", diff --git a/extensions/crossmodel-lang/src/glsp-server/handler/add-entity-operation-handler.ts b/extensions/crossmodel-lang/src/glsp-server/handler/add-entity-operation-handler.ts index a2fd27ed..077cd2eb 100644 --- a/extensions/crossmodel-lang/src/glsp-server/handler/add-entity-operation-handler.ts +++ b/extensions/crossmodel-lang/src/glsp-server/handler/add-entity-operation-handler.ts @@ -16,35 +16,35 @@ import { CrossModelCommand } from './cross-model-command'; */ @injectable() export class CrossModelAddEntityOperationHandler extends OperationHandler { - override operationType = AddEntityOperation.KIND; + override operationType = AddEntityOperation.KIND; - @inject(CrossModelState) protected state: CrossModelState; + @inject(CrossModelState) protected state: CrossModelState; - createCommand(operation: AddEntityOperation): Command { - return new CrossModelCommand(this.state, () => this.createEntityNode(operation)); - } + createCommand(operation: AddEntityOperation): Command { + return new CrossModelCommand(this.state, () => this.createEntityNode(operation)); + } - protected async createEntityNode(operation: AddEntityOperation): Promise { - const container = this.state.diagramRoot; - const refInfo = createNodeToEntityReference(container); - const scope = this.state.services.language.references.ScopeProvider.getScope(refInfo); - const entityDescription = scope.getElement(operation.entityName); - if (entityDescription) { - // create node for entity - const node: DiagramNode = { - $type: DiagramNode, - $container: container, - name: findAvailableNodeName(container, entityDescription.name + 'Node'), - semanticElement: { - $refText: entityDescription.name, - ref: entityDescription.node as Entity | undefined - }, - x: operation.position.x, - y: operation.position.y, - width: 10, - height: 10 - }; - container.nodes.push(node); - } - } + protected async createEntityNode(operation: AddEntityOperation): Promise { + const container = this.state.diagramRoot; + const refInfo = createNodeToEntityReference(container); + const scope = this.state.services.language.references.ScopeProvider.getScope(refInfo); + const entityDescription = scope.getElement(operation.entityName); + if (entityDescription) { + // create node for entity + const node: DiagramNode = { + $type: DiagramNode, + $container: container, + name: findAvailableNodeName(container, entityDescription.name + 'Node'), + for: { + $refText: entityDescription.name, + ref: entityDescription.node as Entity | undefined + }, + x: operation.position.x, + y: operation.position.y, + width: 10, + height: 10 + }; + container.nodes.push(node); + } + } } diff --git a/extensions/crossmodel-lang/src/glsp-server/handler/create-edge-operation-handler.ts b/extensions/crossmodel-lang/src/glsp-server/handler/create-edge-operation-handler.ts index eef38f36..9623279d 100644 --- a/extensions/crossmodel-lang/src/glsp-server/handler/create-edge-operation-handler.ts +++ b/extensions/crossmodel-lang/src/glsp-server/handler/create-edge-operation-handler.ts @@ -3,14 +3,14 @@ ********************************************************************************/ import { - Command, - CreateEdgeOperation, - CreateOperationHandler, - CreateOperationKind, - DefaultTypes, - OperationHandler, - TriggerEdgeCreationAction, - TriggerNodeCreationAction + Command, + CreateEdgeOperation, + CreateOperationHandler, + CreateOperationKind, + DefaultTypes, + OperationHandler, + TriggerEdgeCreationAction, + TriggerNodeCreationAction } from '@eclipse-glsp/server'; import { inject, injectable } from 'inversify'; import { URI, Utils as UriUtils } from 'vscode-uri'; @@ -21,71 +21,68 @@ import { CrossModelCommand } from './cross-model-command'; @injectable() export class CrossModelCreateEdgeOperationHandler extends OperationHandler implements CreateOperationHandler { - override label = '1:1 Relationship'; - elementTypeIds = [DefaultTypes.EDGE]; - operationType: CreateOperationKind = CreateEdgeOperation.KIND; + override label = '1:1 Relationship'; + elementTypeIds = [DefaultTypes.EDGE]; + operationType: CreateOperationKind = CreateEdgeOperation.KIND; - @inject(CrossModelState) protected state: CrossModelState; + @inject(CrossModelState) protected state: CrossModelState; - getTriggerActions(): (TriggerEdgeCreationAction | TriggerNodeCreationAction)[] { - // return trigger actions that are shown in the tool palette on the client - return this.elementTypeIds.map(typeId => TriggerEdgeCreationAction.create(typeId)); - } + getTriggerActions(): (TriggerEdgeCreationAction | TriggerNodeCreationAction)[] { + // return trigger actions that are shown in the tool palette on the client + return this.elementTypeIds.map(typeId => TriggerEdgeCreationAction.create(typeId)); + } - createCommand(operation: CreateEdgeOperation): Command { - return new CrossModelCommand(this.state, () => this.createEdge(operation)); - } + createCommand(operation: CreateEdgeOperation): Command { + return new CrossModelCommand(this.state, () => this.createEdge(operation)); + } - protected async createEdge(operation: CreateEdgeOperation): Promise { - const sourceNode = this.state.index.findDiagramNode(operation.sourceElementId); - const targetNode = this.state.index.findDiagramNode(operation.targetElementId); - if (sourceNode && targetNode) { - // before we can create a digram edge, we need to create the corresponding relationship that it is based on - const relationship = await this.createAndSaveRelationship(sourceNode, targetNode); - if (relationship) { - const edge: DiagramEdge = { - $type: DiagramEdge, - $container: this.state.diagramRoot, - name: relationship.name, - semanticElement: { ref: relationship, $refText: this.state.nameProvider.getName(relationship) || relationship.name }, - source: { ref: sourceNode, $refText: this.state.nameProvider.getLocalName(sourceNode) || sourceNode.name }, - target: { ref: targetNode, $refText: this.state.nameProvider.getLocalName(targetNode) || targetNode.name } - }; - this.state.diagramRoot.edges.push(edge); - } - } - } + protected async createEdge(operation: CreateEdgeOperation): Promise { + const sourceNode = this.state.index.findDiagramNode(operation.sourceElementId); + const targetNode = this.state.index.findDiagramNode(operation.targetElementId); + if (sourceNode && targetNode) { + // before we can create a digram edge, we need to create the corresponding relationship that it is based on + const relationship = await this.createAndSaveRelationship(sourceNode, targetNode); + if (relationship) { + const edge: DiagramEdge = { + $type: DiagramEdge, + $container: this.state.diagramRoot, + name: relationship.name, + for: { ref: relationship, $refText: this.state.nameProvider.getName(relationship) || relationship.name || '' } + }; + this.state.diagramRoot.edges.push(edge); + } + } + } - /** - * Creates a new relationship and stores it on a file on the file system. - */ - protected async createAndSaveRelationship(sourceNode: DiagramNode, targetNode: DiagramNode): Promise { - const source = sourceNode.semanticElement.ref?.name || sourceNode.semanticElement.$refText; - const target = targetNode.semanticElement.ref?.name || targetNode.semanticElement.$refText; + /** + * Creates a new relationship and stores it on a file on the file system. + */ + protected async createAndSaveRelationship(sourceNode: DiagramNode, targetNode: DiagramNode): Promise { + const source = sourceNode.for?.ref?.name || sourceNode.for?.$refText; + const target = targetNode.for?.ref?.name || targetNode.for?.$refText; - // search for unique file name for the relationship and use file base name as relationship name - // if the user doesn't rename any files we should end up with unique names ;-) - const dirName = UriUtils.dirname(URI.parse(this.state.semanticUri)); - const targetUri = UriUtils.joinPath(dirName, source + 'To' + target + '.relationship.cm'); - const uri = Utils.findNewUri(targetUri); - const name = UriUtils.basename(uri).split('.')[0]; + // search for unique file name for the relationship and use file base name as relationship name + // if the user doesn't rename any files we should end up with unique names ;-) + const dirName = UriUtils.dirname(URI.parse(this.state.semanticUri)); + const targetUri = UriUtils.joinPath(dirName, source + 'To' + target + '.relationship.cm'); + const uri = Utils.findNewUri(targetUri); + const name = UriUtils.basename(uri).split('.')[0]; - // create relationship, serialize and re-read to ensure everything is up to date and linked properly - const relationshipRoot: CrossModelRoot = { $type: 'CrossModelRoot' }; - const relationship: Relationship = { - $type: Relationship, - $container: relationshipRoot, - name, - type: '1:1', - properties: [], - source: { $refText: sourceNode.semanticElement.$refText }, - target: { $refText: targetNode.semanticElement.$refText } - }; - relationshipRoot.relationship = relationship; - const text = this.state.semanticSerializer.serialize(relationshipRoot); + // create relationship, serialize and re-read to ensure everything is up to date and linked properly + const relationshipRoot: CrossModelRoot = { $type: 'CrossModelRoot' }; + const relationship: Relationship = { + $type: Relationship, + $container: relationshipRoot, + name, + type: '1:1', + parent: { $refText: sourceNode.for?.$refText || '' }, + child: { $refText: targetNode.for?.$refText || '' } + }; + relationshipRoot.relationship = relationship; + const text = this.state.semanticSerializer.serialize(relationshipRoot); - await this.state.modelService.save(uri.toString(), text); - const root = await this.state.modelService.request(uri.toString(), isCrossModelRoot); - return root?.relationship; - } + await this.state.modelService.save(uri.toString(), text); + const root = await this.state.modelService.request(uri.toString(), isCrossModelRoot); + return root?.relationship; + } } diff --git a/extensions/crossmodel-lang/src/glsp-server/handler/drop-entity-operation-handler.ts b/extensions/crossmodel-lang/src/glsp-server/handler/drop-entity-operation-handler.ts index 3b9f6633..609ccb93 100644 --- a/extensions/crossmodel-lang/src/glsp-server/handler/drop-entity-operation-handler.ts +++ b/extensions/crossmodel-lang/src/glsp-server/handler/drop-entity-operation-handler.ts @@ -18,37 +18,37 @@ import { CrossModelCommand } from './cross-model-command'; */ @injectable() export class CrossModelDropEntityOperationHandler extends OperationHandler { - override operationType = DropEntityOperation.KIND; + override operationType = DropEntityOperation.KIND; - @inject(CrossModelState) protected state: CrossModelState; + @inject(CrossModelState) protected state: CrossModelState; - createCommand(operation: DropEntityOperation): Command { - return new CrossModelCommand(this.state, () => this.createEntityNode(operation)); - } + createCommand(operation: DropEntityOperation): Command { + return new CrossModelCommand(this.state, () => this.createEntityNode(operation)); + } - protected async createEntityNode(operation: DropEntityOperation): Promise { - const container = this.state.diagramRoot; - let x = operation.position.x; - let y = operation.position.y; - for (const filePath of operation.filePaths) { - const root = await this.state.modelService.request(URI.file(filePath).toString(), isCrossModelRoot); - if (root?.entity) { - // create node for entity - const node: DiagramNode = { - $type: DiagramNode, - $container: container, - name: findAvailableNodeName(container, root.entity.name + 'Node'), - semanticElement: { - $refText: this.state.nameProvider.getFullyQualifiedName(root.entity) || root.entity.name, - ref: root.entity - }, - x: (x += 10), - y: (y += 10), - width: 10, - height: 10 - }; - container.nodes.push(node); - } - } - } + protected async createEntityNode(operation: DropEntityOperation): Promise { + const container = this.state.diagramRoot; + let x = operation.position.x; + let y = operation.position.y; + for (const filePath of operation.filePaths) { + const root = await this.state.modelService.request(URI.file(filePath).toString(), isCrossModelRoot); + if (root?.entity) { + // create node for entity + const node: DiagramNode = { + $type: DiagramNode, + $container: container, + name: findAvailableNodeName(container, root.entity.name + 'Node'), + for: { + $refText: this.state.nameProvider.getFullyQualifiedName(root.entity) || root.entity.name || '', + ref: root.entity + }, + x: (x += 10), + y: (y += 10), + width: 10, + height: 10 + }; + container.nodes.push(node); + } + } + } } diff --git a/extensions/crossmodel-lang/src/glsp-server/model/builders/gentity-node.ts b/extensions/crossmodel-lang/src/glsp-server/model/builders/gentity-node.ts index b01a1362..ae12aba6 100644 --- a/extensions/crossmodel-lang/src/glsp-server/model/builders/gentity-node.ts +++ b/extensions/crossmodel-lang/src/glsp-server/model/builders/gentity-node.ts @@ -1,75 +1,73 @@ /******************************************************************************** * Copyright (c) 2023 CrossBreeze. ********************************************************************************/ -import { ArgsUtil, GCompartment, GLabel, GNode, GNodeBuilder } from '@eclipse-glsp/server'; -import { DiagramNode } from '../../../language-server/generated/ast'; -export class GEntityNode extends GNode { - override type = 'node:entity'; +// export class GEntityNode extends GNode { +// override type = 'node:entity'; - static override builder(): GEntityNodeBuilder { - return new GEntityNodeBuilder(GEntityNode).type('node:entity'); - } -} +// static override builder(): GEntityNodeBuilder { +// return new GEntityNodeBuilder(GEntityNode).type('node:entity'); +// } +// } -export class GEntityNodeBuilder extends GNodeBuilder { - addNode(node: DiagramNode): this { - // Get the reference that the DiagramNode holds to the Entity in the .langium file. - const entityRef = node.semanticElement.ref; +// export class GEntityNodeBuilder extends GNodeBuilder { +// addNode(node: DiagramNode): this { +// // Get the reference that the DiagramNode holds to the Entity in the .langium file. +// const entityRef = node.semanticElement.ref; - // Options which are the same for every node - this.addCssClasses('diagram-node', 'entity').layout('vbox').addArgs(ArgsUtil.cornerRadius(3)); +// // Options which are the same for every node +// this.addCssClasses('diagram-node', 'entity').layout('vbox').addArgs(ArgsUtil.cornerRadius(3)); - // We need the id before we can build the label and childeren. - if (this.id === undefined) { - throw new Error('Add id to builder before adding the node reference.'); - } +// // We need the id before we can build the label and childeren. +// if (this.id === undefined) { +// throw new Error('Add id to builder before adding the node reference.'); +// } - // Add the label/name of the node - const label = GCompartment.builder() - .layout('hbox') - .addLayoutOption('hAlign', 'center') - .addCssClass('entity-header-compartment') - .add( - GLabel.builder() - .text(entityRef?.name || 'unresolved') - .id(`${this.proxy.id}_label`) - .addCssClass('entity-header-label') - .build() - ) - .build(); +// // Add the label/name of the node +// const label = GCompartment.builder() +// .layout('hbox') +// .addLayoutOption('hAlign', 'center') +// .addCssClass('entity-header-compartment') +// .add( +// GLabel.builder() +// .text(entityRef?.name || 'unresolved') +// .id(`${this.proxy.id}_label`) +// .addCssClass('entity-header-label') +// .build() +// ) +// .build(); - this.add(label); +// this.add(label); - // Add the children of the node - if (entityRef !== undefined) { - const allAttributesCompartment = GCompartment.builder() - .addCssClass('attributes-compartment') - .layout('vbox') - .addLayoutOption('hAlign', 'left') - .addLayoutOption('paddingBottom', 0); +// // Add the children of the node +// if (entityRef !== undefined) { +// const allAttributesCompartment = GCompartment.builder() +// .addCssClass('attributes-compartment') +// .layout('vbox') +// .addLayoutOption('hAlign', 'left') +// .addLayoutOption('paddingBottom', 0); - // Add the attributes of the entity. - for (const attribute of entityRef.attributes) { - const attributeCompartment = GCompartment.builder() - .addCssClass('attribute-compartment') - .layout('hbox') - .addLayoutOption('paddingBottom', 3) - .addLayoutOption('paddingTop', 3); +// // Add the attributes of the entity. +// for (const attribute of entityRef.attributes) { +// const attributeCompartment = GCompartment.builder() +// .addCssClass('attribute-compartment') +// .layout('hbox') +// .addLayoutOption('paddingBottom', 3) +// .addLayoutOption('paddingTop', 3); - attributeCompartment.add(GLabel.builder().text(attribute.name).addCssClass('attribute').build()); - attributeCompartment.add(GLabel.builder().text(' : ').build()); - attributeCompartment.add(GLabel.builder().text(attribute.value.toString()).addCssClass('datatype').build()); +// attributeCompartment.add(GLabel.builder().text(attribute.name).addCssClass('attribute').build()); +// attributeCompartment.add(GLabel.builder().text(' : ').build()); +// attributeCompartment.add(GLabel.builder().text(attribute.value.toString()).addCssClass('datatype').build()); - allAttributesCompartment.add(attributeCompartment.build()); - } +// allAttributesCompartment.add(attributeCompartment.build()); +// } - this.add(allAttributesCompartment.build()); - } +// this.add(allAttributesCompartment.build()); +// } - // The DiagramNode in the langium file holds the coordinates of node - this.addLayoutOption('prefWidth', node.width).addLayoutOption('prefHeight', node.height).position(node.x, node.y); +// // The DiagramNode in the langium file holds the coordinates of node +// this.addLayoutOption('prefWidth', node.width).addLayoutOption('prefHeight', node.height).position(node.x, node.y); - return this; - } -} +// return this; +// } +// } diff --git a/extensions/crossmodel-lang/src/glsp-server/model/cross-model-gmodel-factory.ts b/extensions/crossmodel-lang/src/glsp-server/model/cross-model-gmodel-factory.ts index 172afb28..ad998ca7 100644 --- a/extensions/crossmodel-lang/src/glsp-server/model/cross-model-gmodel-factory.ts +++ b/extensions/crossmodel-lang/src/glsp-server/model/cross-model-gmodel-factory.ts @@ -1,10 +1,10 @@ /******************************************************************************** * Copyright (c) 2023 CrossBreeze. ********************************************************************************/ -import { GEdge, GGraph, GModelFactory, GNode } from '@eclipse-glsp/server'; +import { GModelFactory } from '@eclipse-glsp/server'; import { inject, injectable } from 'inversify'; -import { DiagramEdge, DiagramNode } from '../../language-server/generated/ast'; -import { GEntityNode } from './builders/gentity-node'; +// import { DiagramEdge, DiagramNode } from '../../language-server/generated/ast'; +// import { GEntityNode } from './builders/gentity-node'; import { CrossModelState } from './cross-model-state'; /** @@ -14,39 +14,39 @@ import { CrossModelState } from './cross-model-state'; */ @injectable() export class CrossModelGModelFactory implements GModelFactory { - @inject(CrossModelState) protected readonly modelState: CrossModelState; + @inject(CrossModelState) protected readonly modelState: CrossModelState; - createModel(): void { - const newRoot = this.createGraph(); - if (newRoot) { - // update GLSP root element in state so it can be used in any follow-up actions/commands - this.modelState.updateRoot(newRoot); - } - } + createModel(): void { + // const newRoot = this.createGraph(); + // if (newRoot) { + // // update GLSP root element in state so it can be used in any follow-up actions/commands + // this.modelState.updateRoot(newRoot); + // } + } - protected createGraph(): GGraph | undefined { - const diagramRoot = this.modelState.diagramRoot; + // protected createGraph(): GGraph | undefined { + // const diagramRoot = this.modelState.diagramRoot; - const graphBuilder = GGraph.builder().id(this.modelState.semanticUri); - diagramRoot.nodes.map(node => this.createDiagramNode(node)).forEach(node => graphBuilder.add(node)); - diagramRoot.edges.map(edge => this.createDiagramEdge(edge)).forEach(edge => graphBuilder.add(edge)); + // const graphBuilder = GGraph.builder().id(this.modelState.semanticUri); + // diagramRoot.nodes.map(node => this.createDiagramNode(node)).forEach(node => graphBuilder.add(node)); + // diagramRoot.edges.map(edge => this.createDiagramEdge(edge)).forEach(edge => graphBuilder.add(edge)); - return graphBuilder.build(); - } + // return graphBuilder.build(); + // } - protected createDiagramNode(node: DiagramNode): GNode { - // Get the reference that the DiagramNode holds to the Entity in the .langium file. - const id = this.modelState.index.createId(node) ?? 'unknown'; - return GEntityNode.builder().id(id).addNode(node).build(); - } + // protected createDiagramNode(node: DiagramNode): GNode { + // // Get the reference that the DiagramNode holds to the Entity in the .langium file. + // const id = this.modelState.index.createId(node) ?? 'unknown'; + // return GEntityNode.builder().id(id).addNode(node).build(); + // } - protected createDiagramEdge(edge: DiagramEdge): GEdge { - const id = this.modelState.index.createId(edge) ?? 'unknown'; - return GEdge.builder() - .id(id) - .addCssClasses('diagram-edge', 'relationship') - .sourceId(edge.source.$refText) - .targetId(edge.target.$refText) - .build(); - } + // protected createDiagramEdge(edge: DiagramEdge): GEdge { + // const id = this.modelState.index.createId(edge) ?? 'unknown'; + // return GEdge.builder() + // .id(id) + // .addCssClasses('diagram-edge', 'relationship') + // .sourceId(edge.source.$refText) + // .targetId(edge.target.$refText) + // .build(); + // } } diff --git a/extensions/crossmodel-lang/src/glsp-server/model/cross-model-state.ts b/extensions/crossmodel-lang/src/glsp-server/model/cross-model-state.ts index 4e449f85..684d1da5 100644 --- a/extensions/crossmodel-lang/src/glsp-server/model/cross-model-state.ts +++ b/extensions/crossmodel-lang/src/glsp-server/model/cross-model-state.ts @@ -7,7 +7,7 @@ import { URI } from 'vscode-uri'; import { QualifiedNameProvider } from '../../language-server/cross-model-naming'; import { CrossModelRoot, SystemDiagram } from '../../language-server/generated/ast'; import { ModelService } from '../../model-server/model-service'; -import { DiagramSerializer } from '../../model-server/serializer'; +import { Serializer } from '../../model-server/serializer'; import { CrossModelLSPServices } from '../integration'; import { CrossModelIndex } from './cross-model-index'; @@ -17,55 +17,55 @@ import { CrossModelIndex } from './cross-model-index'; */ @injectable() export class CrossModelState extends DefaultModelState { - @inject(CrossModelIndex) override readonly index: CrossModelIndex; - @inject(CrossModelLSPServices) readonly services: CrossModelLSPServices; + @inject(CrossModelIndex) override readonly index: CrossModelIndex; + @inject(CrossModelLSPServices) readonly services: CrossModelLSPServices; - protected _semanticUri: string; - protected _semanticRoot: CrossModelRoot; - protected _packageId: string; + protected _semanticUri: string; + protected _semanticRoot: CrossModelRoot; + protected _packageId: string; - setSemanticRoot(uri: string, semanticRoot: CrossModelRoot): void { - this._semanticUri = uri; - this._semanticRoot = semanticRoot; - this._packageId = this.services.shared.workspace.PackageManager.getPackageIdByUri(URI.parse(uri)); - this.index.indexSemanticRoot(this.semanticRoot); - } + setSemanticRoot(uri: string, semanticRoot: CrossModelRoot): void { + this._semanticUri = uri; + this._semanticRoot = semanticRoot; + this._packageId = this.services.shared.workspace.PackageManager.getPackageIdByUri(URI.parse(uri)); + this.index.indexSemanticRoot(this.semanticRoot); + } - get semanticUri(): string { - return this._semanticUri; - } + get semanticUri(): string { + return this._semanticUri; + } - get semanticRoot(): CrossModelRoot { - return this._semanticRoot; - } + get semanticRoot(): CrossModelRoot { + return this._semanticRoot; + } - get packageId(): string { - return this._packageId; - } + get packageId(): string { + return this._packageId; + } - get diagramRoot(): SystemDiagram { - return this.semanticRoot.diagram!; - } + get diagramRoot(): SystemDiagram { + return this.semanticRoot.diagram!; + } - get modelService(): ModelService { - return this.services.shared.model.ModelService; - } + get modelService(): ModelService { + return this.services.shared.model.ModelService; + } - get semanticSerializer(): DiagramSerializer { - return this.services.language.serializer.Serializer; - } + get semanticSerializer(): Serializer { + return this.services.language.serializer.Serializer; + } - get nameProvider(): QualifiedNameProvider { - return this.services.language.references.QualifiedNameProvider; - } + get nameProvider(): QualifiedNameProvider { + return this.services.language.references.QualifiedNameProvider; + } - async updateSemanticRoot(content?: string): Promise { - this._semanticRoot = await this.modelService.update(this.semanticUri, content ?? this.semanticRoot); - this.index.indexSemanticRoot(this.semanticRoot); - } + async updateSemanticRoot(content?: string): Promise { + this._semanticRoot = await this.modelService.update(this.semanticUri, content ?? this.semanticRoot); + this.index.indexSemanticRoot(this.semanticRoot); + } - /** Textual representation of the current semantic root. */ - semanticText(): string { - return this.services.language.serializer.Serializer.serialize(this.semanticRoot); - } + /** Textual representation of the current semantic root. */ + semanticText(): string { + return this.services.language.serializer.Serializer.serialize(this.semanticRoot); + } } diff --git a/extensions/crossmodel-lang/src/language-server/cross-model-formatter.ts b/extensions/crossmodel-lang/src/language-server/cross-model-formatter.ts index 8b25970f..709fa858 100644 --- a/extensions/crossmodel-lang/src/language-server/cross-model-formatter.ts +++ b/extensions/crossmodel-lang/src/language-server/cross-model-formatter.ts @@ -1,39 +1,10 @@ /******************************************************************************** * Copyright (c) 2023 CrossBreeze. ********************************************************************************/ -import { AbstractFormatter, AstNode, Formatting } from 'langium'; - -const ROOT_KEYWORDS = ['entity', 'relationship', 'diagram']; -const OTHER_KEYWORDS = [ - 'description', - 'attributes', - 'source', - 'target', - 'type', - 'properties', - ':=', - 'node', - 'edge', - 'for', - 'x', - 'y', - 'width', - 'height', - 'source', - 'target' -]; +import { AbstractFormatter, AstNode } from 'langium'; export class CrossModelModelFormatter extends AbstractFormatter { - protected format(node: AstNode): void { - const formatter = this.getNodeFormatter(node); - formatter.keywords(...ROOT_KEYWORDS).prepend(Formatting.noSpace({ allowLess: false, allowMore: false, priority: 1 })); - formatter.keywords(...OTHER_KEYWORDS).surround(Formatting.oneSpace({ allowLess: false, allowMore: false, priority: 1 })); - formatter - .interior(formatter.keyword('{'), formatter.keyword('}')) - .prepend(Formatting.indent({ allowLess: false, allowMore: false, priority: 1 })); - formatter - .keywords(';') - .prepend(Formatting.noSpace({ allowLess: false, allowMore: false, priority: 1 })) - .append(Formatting.newLine()); - } + protected format(node: AstNode): void { + return; + } } diff --git a/extensions/crossmodel-lang/src/language-server/cross-model-module.ts b/extensions/crossmodel-lang/src/language-server/cross-model-module.ts index 75f2e615..faca6350 100644 --- a/extensions/crossmodel-lang/src/language-server/cross-model-module.ts +++ b/extensions/crossmodel-lang/src/language-server/cross-model-module.ts @@ -2,19 +2,19 @@ * Copyright (c) 2023 CrossBreeze. ********************************************************************************/ import { - AstNode, - createDefaultModule, - createDefaultSharedModule, - DefaultServiceRegistry, - DefaultSharedModuleContext, - inject, - JsonSerializer, - LangiumServices, - LangiumSharedServices, - Module, - PartialLangiumServices, - PartialLangiumSharedServices, - ServiceRegistry + AstNode, + createDefaultModule, + createDefaultSharedModule, + DefaultServiceRegistry, + DefaultSharedModuleContext, + inject, + JsonSerializer, + LangiumServices, + LangiumSharedServices, + Module, + PartialLangiumServices, + PartialLangiumSharedServices, + ServiceRegistry } from 'langium'; import { TextDocument } from 'vscode-languageserver-textdocument'; import { URI } from 'vscode-uri'; @@ -36,70 +36,74 @@ import { CrossModelSerializer } from './cross-model-serializer'; import { CrossModelValidator, registerValidationChecks } from './cross-model-validator'; import { CrossModelWorkspaceManager } from './cross-model-workspace-manager'; import { CrossModelGeneratedModule, CrossModelGeneratedSharedModule } from './generated/module'; +import { CrossModelTokenBuilder } from './lexer/cross-model-token-generator'; +import { CrossModelLexer } from './lexer/cross-model-lexer'; /*************************** * Shared Module ***************************/ export interface ExtendedLangiumServices extends LangiumServices { - serializer: { - JsonSerializer: JsonSerializer; - Serializer: Serializer; - }; + serializer: { + JsonSerializer: JsonSerializer; + Serializer: Serializer; + }; } export class ExtendedServiceRegistry extends DefaultServiceRegistry { - override register(language: ExtendedLangiumServices): void { - super.register(language); - } + override register(language: ExtendedLangiumServices): void { + super.register(language); + } - override getServices(uri: URI): ExtendedLangiumServices { - return super.getServices(uri) as ExtendedLangiumServices; - } + override getServices(uri: URI): ExtendedLangiumServices { + return super.getServices(uri) as ExtendedLangiumServices; + } } export interface ExtendedServiceRegistry extends ServiceRegistry { - register(language: ExtendedLangiumServices): void; - getServices(uri: URI): ExtendedLangiumServices; + register(language: ExtendedLangiumServices): void; + getServices(uri: URI): ExtendedLangiumServices; } /** * Declaration of custom services - add your own service classes here. */ export interface CrossModelAddedSharedServices { - /* override */ ServiceRegistry: ExtendedServiceRegistry; - workspace: { - /* override */ WorkspaceManager: CrossModelWorkspaceManager; - PackageManager: CrossModelPackageManager; - }; - logger: { - ClientLogger: ClientLogger; - }; + /* override */ + ServiceRegistry: ExtendedServiceRegistry; + + workspace: { + /* override */ WorkspaceManager: CrossModelWorkspaceManager; + PackageManager: CrossModelPackageManager; + }; + logger: { + ClientLogger: ClientLogger; + }; } export const CrossModelSharedServices = Symbol('CrossModelSharedServices'); export type CrossModelSharedServices = Omit & - CrossModelAddedSharedServices & - AddedSharedModelServices; + CrossModelAddedSharedServices & + AddedSharedModelServices; export const CrossModelSharedModule: Module< - CrossModelSharedServices, - PartialLangiumSharedServices & CrossModelAddedSharedServices & AddedSharedModelServices + CrossModelSharedServices, + PartialLangiumSharedServices & CrossModelAddedSharedServices & AddedSharedModelServices > = { - ServiceRegistry: () => new ExtendedServiceRegistry(), - workspace: { - WorkspaceManager: services => new CrossModelWorkspaceManager(services), - PackageManager: services => new CrossModelPackageManager(services), - LangiumDocuments: services => new CrossModelLangiumDocuments(services), - TextDocuments: () => new OpenableTextDocuments(TextDocument), - TextDocumentManager: services => new OpenTextDocumentManager(services), - DocumentBuilder: services => new CrossModelDocumentBuilder(services) - }, - logger: { - ClientLogger: services => new ClientLogger(services) - }, - model: { - ModelService: services => new ModelService(services) - } + ServiceRegistry: () => new ExtendedServiceRegistry(), + workspace: { + WorkspaceManager: services => new CrossModelWorkspaceManager(services), + PackageManager: services => new CrossModelPackageManager(services), + LangiumDocuments: services => new CrossModelLangiumDocuments(services), + TextDocuments: () => new OpenableTextDocuments(TextDocument), + TextDocumentManager: services => new OpenTextDocumentManager(services), + DocumentBuilder: services => new CrossModelDocumentBuilder(services) + }, + logger: { + ClientLogger: services => new ClientLogger(services) + }, + model: { + ModelService: services => new ModelService(services) + } }; /*************************** @@ -107,23 +111,23 @@ export const CrossModelSharedModule: Module< ***************************/ export interface CrossModelModuleContext { - shared: CrossModelSharedServices; + shared: CrossModelSharedServices; } /** * Declaration of custom services - add your own service classes here. */ export interface CrossModelAddedServices { - references: { - QualifiedNameProvider: QualifiedNameProvider; - }; - validation: { - CrossModelValidator: CrossModelValidator; - }; - serializer: { - Serializer: CrossModelSerializer; - }; - /* override */ shared: CrossModelSharedServices; + references: { + QualifiedNameProvider: QualifiedNameProvider; + }; + validation: { + CrossModelValidator: CrossModelValidator; + }; + serializer: { + Serializer: CrossModelSerializer; + }; + /* override */ shared: CrossModelSharedServices; } /** @@ -139,26 +143,30 @@ export const CrossModelServices = Symbol('CrossModelServices'); * selected services, while the custom services must be fully specified. */ export function createCrossModelModule( - context: CrossModelModuleContext + context: CrossModelModuleContext ): Module { - return { - references: { - ScopeComputation: services => new CrossModelScopeComputation(services), - ScopeProvider: services => new CrossModelScopeProvider(services), - QualifiedNameProvider: services => new QualifiedNameProvider(services) - }, - validation: { - CrossModelValidator: () => new CrossModelValidator() - }, - lsp: { - CompletionProvider: services => new CrossModelCompletionProvider(services), - Formatter: () => new CrossModelModelFormatter() - }, - serializer: { - Serializer: services => new CrossModelSerializer(services) - }, - shared: () => context.shared - }; + return { + references: { + ScopeComputation: services => new CrossModelScopeComputation(services), + ScopeProvider: services => new CrossModelScopeProvider(services), + QualifiedNameProvider: services => new QualifiedNameProvider(services) + }, + validation: { + CrossModelValidator: () => new CrossModelValidator() + }, + lsp: { + CompletionProvider: services => new CrossModelCompletionProvider(services), + Formatter: () => new CrossModelModelFormatter() + }, + serializer: { + Serializer: services => new CrossModelSerializer(services) + }, + parser: { + TokenBuilder: () => new CrossModelTokenBuilder(), + Lexer: services => new CrossModelLexer(services) + }, + shared: () => context.shared + }; } /** @@ -177,12 +185,12 @@ export function createCrossModelModule( * @returns An object wrapping the shared services and the language-specific services */ export function createCrossModelServices(context: DefaultSharedModuleContext): { - shared: CrossModelSharedServices; - CrossModel: CrossModelServices; + shared: CrossModelSharedServices; + CrossModel: CrossModelServices; } { - const shared = inject(createDefaultSharedModule(context), CrossModelGeneratedSharedModule, CrossModelSharedModule); - const CrossModel = inject(createDefaultModule({ shared }), CrossModelGeneratedModule, createCrossModelModule({ shared })); - shared.ServiceRegistry.register(CrossModel); - registerValidationChecks(CrossModel); - return { shared, CrossModel }; + const shared = inject(createDefaultSharedModule(context), CrossModelGeneratedSharedModule, CrossModelSharedModule); + const CrossModel = inject(createDefaultModule({ shared }), CrossModelGeneratedModule, createCrossModelModule({ shared })); + shared.ServiceRegistry.register(CrossModel); + registerValidationChecks(CrossModel); + return { shared, CrossModel }; } diff --git a/extensions/crossmodel-lang/src/language-server/cross-model-serializer.ts b/extensions/crossmodel-lang/src/language-server/cross-model-serializer.ts index 9807118a..a9df8335 100644 --- a/extensions/crossmodel-lang/src/language-server/cross-model-serializer.ts +++ b/extensions/crossmodel-lang/src/language-server/cross-model-serializer.ts @@ -2,160 +2,106 @@ * Copyright (c) 2023 CrossBreeze. ********************************************************************************/ -import { isReference, Reference } from 'langium'; -import { DiagramSerializer, Serializer } from '../model-server/serializer'; +import { AstNode, isReference } from 'langium'; +import { Serializer } from '../model-server/serializer'; import { CrossModelServices } from './cross-model-module'; -import { - Attribute, - CrossModelRoot, - DiagramEdge, - DiagramNode, - Entity, - isCrossModelRoot, - isEntity, - isSystemDiagram, - Property, - Relationship, - SystemDiagram -} from './generated/ast'; +import { CrossModelRoot } from './generated/ast'; /** * Hand-written AST serializer as there is currently no out-of-the box serializer from Langium, but it is on the roadmap. * cf. https://github.com/langium/langium/discussions/683 * cf. https://github.com/langium/langium/discussions/863 */ -export class CrossModelSerializer implements Serializer, DiagramSerializer { +export class CrossModelSerializer implements Serializer { constructor(protected services: CrossModelServices, protected refNameProvider = services.references.QualifiedNameProvider) {} serialize(root: CrossModelRoot): string { + let newRoot: AstNode | undefined = this.toSerializableObject(root); + + let startKey; + if (root.entity) { - return this.serializeEntity(root.entity); - } - if (root.relationship) { - return this.serializeRelationship(root.relationship); + startKey = 'entity'; + newRoot = root.entity; + } else if (root.diagram) { + startKey = 'diagram'; + newRoot = root.diagram; + } else if (root.relationship) { + startKey = 'relationship'; + newRoot = root.diagram; + } else { + return ''; } - if (root.diagram) { - return this.serializeDiagram(root.diagram); - } - return ''; - } - protected serializeEntity(entity: Entity): string { - return `entity ${entity.name} { - description := "${entity.description}"; - attributes { -${this.serializeAttributes(entity.attributes as Array)} } -}`; + return startKey + ':' + '\n ' + this.serializeValue(newRoot, 0); } - private serializeAttributes(attributes: Array | undefined): string { - let result = ''; - - if (attributes) { - for (const [, attribute] of Object.entries(attributes)) { - result += `\t\t${attribute.name} := '${attribute.value}';\n`; - } + private serializeValue(value: any, indentationLevel: number): string { + if (typeof value === 'object' && value !== undefined) { + return this.serializeObject(value, indentationLevel + 4); + } else if (Array.isArray(value)) { + return this.serializeArray(value, indentationLevel + 4); + } else { + return JSON.stringify(value); } - - return result; } - protected serializeRelationship(relationship: Relationship): string { - return `relationship ${relationship.name} { - source := ${this.serializeReference(relationship.source)}${ - relationship.sourceAttribute ? ' with ' + this.serializeReference(relationship.sourceAttribute) : '' - }; - target := ${this.serializeReference(relationship.target)}${ - relationship.targetAttribute ? ' with ' + this.serializeReference(relationship.targetAttribute) : '' - }; - type := ${relationship.type}; - properties { - ${this.serializeProperties(relationship.properties)} - } - }`; - } + private serializeObject(obj: Record, indentationLevel: number): string { + const indentation = ' '.repeat(indentationLevel); - private serializeProperties(properties: Property[] | undefined): string { - return properties && Array.isArray(properties) - ? properties.map(property => `${property.key} := ${property.value}`).join(';\n') - : ''; - } + const serializedProperties = Object.entries(obj).map(([key, value]) => { + const serializedValue = this.serializeValue(value, indentationLevel); + return `${indentation}${key}: ${serializedValue}`; + }); - protected serializeReference(reference: Reference | string | undefined): string { - if (reference === undefined) { - return ''; - } - return isReference(reference) ? reference.$refText : reference; + return serializedProperties.join(',\n') + '\n'; } - protected serializeDiagram(diagram: SystemDiagram): string { - return `diagram { - ${diagram.nodes.map(node => this.serializeDiagramNode(node)).join('\n')} - ${diagram.edges.map(edge => this.serializeDiagramEdge(edge)).join('\n')} - }`; - } + private serializeArray(arr: any[], indentationLevel: number): string { + let serializedItems = arr.map(item => this.serializeValue(item, indentationLevel)).join('\n'); + serializedItems = this.changeCharInString(serializedItems, indentationLevel - 2, '-'); - protected serializeDiagramNode(node: DiagramNode): string { - return `node ${node.name} for ${node.semanticElement.$refText} { - x := ${node.x}; - y := ${node.y}; - width := ${node.width}; - height := ${node.height}; - };`; + return serializedItems + '\n'; } - protected serializeDiagramEdge(edge: DiagramEdge): string { - return `edge ${edge.name} for ${edge.semanticElement.$refText} { - source := ${edge.source.$refText}; - target := ${edge.target.$refText}; - };`; + private changeCharInString(inputString: string, indexToChange: number, newChar: any): string { + if (indexToChange < 0 || indexToChange >= inputString.length) { + throw Error('invalid'); + } + + const modifiedString = inputString.slice(0, indexToChange) + newChar + inputString.slice(indexToChange + 1); + return modifiedString; } - asDiagram(element: SystemDiagram | Entity | Relationship | CrossModelRoot): string { - if (isCrossModelRoot(element)) { - return element.entity - ? this.asDiagram(element.entity) - : element.relationship - ? this.asDiagram(element.relationship) - : element.diagram - ? this.asDiagram(element.diagram) - : 'diagram { }'; - } - if (isSystemDiagram(element)) { - return this.serializeDiagram(element); + /** + * Cleans the semantic object of any property that cannot be serialized as a String and thus cannot be sent to the client + * over the RPC connection. + * + * @param obj semantic object + * @returns serializable semantic object + */ + toSerializableObject(obj?: T): T | undefined { + if (!obj) { + return; } - if (isEntity(element)) { - return `diagram { - ${this.asDiagramNode(element)}; - }`; - } - if (!element.source.ref || !element.target.ref) { - return 'diagram { }'; - } - return `diagram { - ${this.asDiagramNode(element.source.ref)}; - ${this.asDiagramNode(element.target.ref)}; - ${this.asDiagramEdge(element)} - }`; + + return Object.entries(obj) + .filter(([key, value]) => !key.startsWith('$')) + .reduce((acc, [key, value]) => ({ ...acc, [key]: this.cleanValue(value) }), {}); } - protected toDiagramName(element: Entity | Relationship): string { - return isEntity(element) ? element.name + '_node' : element.name + '_edge'; + cleanValue(value: any): any { + return this.isContainedObject(value) ? this.toSerializableObject(value) : this.resolvedValue(value); } - protected asDiagramNode(entity: Entity): string { - return `node ${this.toDiagramName(entity)} for ${this.refNameProvider.getName(entity)} { - x := 0; - y := 0; - width := 100; - height := 100; - }`; + isContainedObject(value: any): boolean { + return value === Object(value) && !isReference(value); } - protected asDiagramEdge(relationship: Relationship): string { - return `edge ${this.toDiagramName(relationship)} for ${this.refNameProvider.getName(relationship)} { - source := ${this.toDiagramName(relationship.source.ref!)}; - target := ${this.toDiagramName(relationship.target.ref!)}; - }`; + resolvedValue(value: any): any { + if (isReference(value)) { + return value.$refText; + } + return value; } } diff --git a/extensions/crossmodel-lang/src/language-server/cross-model-validator.ts b/extensions/crossmodel-lang/src/language-server/cross-model-validator.ts index 9a931195..3498ae1d 100644 --- a/extensions/crossmodel-lang/src/language-server/cross-model-validator.ts +++ b/extensions/crossmodel-lang/src/language-server/cross-model-validator.ts @@ -3,44 +3,51 @@ ********************************************************************************/ import { ValidationAcceptor, ValidationChecks } from 'langium'; import type { CrossModelServices } from './cross-model-module'; -import { CrossModelAstType, Entity, Relationship } from './generated/ast'; +import { CrossModelAstType, Entity, EntityAttribute, Relationship, SystemDiagram } from './generated/ast'; /** * Register custom validation checks. */ export function registerValidationChecks(services: CrossModelServices): void { - const registry = services.validation.ValidationRegistry; - const validator = services.validation.CrossModelValidator; - const checks: ValidationChecks = { - Entity: validator.checkEntityStartsWithCapital, - Relationship: validator.checkRelationshipAttributes - }; - registry.register(checks, validator); + const registry = services.validation.ValidationRegistry; + const validator = services.validation.CrossModelValidator; + + const checks: ValidationChecks = { + Entity: validator.checkEntityHasNecessaryFields, + EntityAttribute: validator.checkAttributeHasNecessaryFields, + SystemDiagram: validator.checkSystemDiagramHasNecessaryFields, + Relationship: validator.checkRelationshipHasNecessaryFields + }; + registry.register(checks, validator); } /** * Implementation of custom validations. */ export class CrossModelValidator { - checkEntityStartsWithCapital(entity: Entity, accept: ValidationAcceptor): void { - if (entity.name) { - const firstChar = entity.name.substring(0, 1); - if (firstChar.toUpperCase() !== firstChar) { - accept('warning', 'Entity name should start with a capital.', { node: entity, property: 'name' }); - } - } - } + checkSystemDiagramHasNecessaryFields(system: SystemDiagram, accept: ValidationAcceptor): void { + console.log('test', system); + if (!system.name) { + accept('error', 'Systemdiagram missing id field', { node: system, property: 'name' }); + } + } + + checkEntityHasNecessaryFields(entity: Entity, accept: ValidationAcceptor): void { + console.log('test', entity); + if (!entity.name) { + accept('error', 'Entity missing id field', { node: entity, property: 'name' }); + } + } + + checkAttributeHasNecessaryFields(attribute: EntityAttribute, accept: ValidationAcceptor): void { + if (!attribute.name) { + accept('error', 'Attribute missing id field', { node: attribute, property: 'name' }); + } + } - checkRelationshipAttributes(relationship: Relationship, accept: ValidationAcceptor): void { - if (relationship.sourceAttribute) { - if (relationship.sourceAttribute.ref?.$container !== relationship.source.ref) { - accept('error', 'Source attribute must come from source entity.', { node: relationship, property: 'sourceAttribute' }); - } - } - if (relationship.targetAttribute) { - if (relationship.targetAttribute.ref?.$container !== relationship.target.ref) { - accept('error', 'Target attribute must come from target entity.', { node: relationship, property: 'targetAttribute' }); - } - } - } + checkRelationshipHasNecessaryFields(relationship: Relationship, accept: ValidationAcceptor): void { + if (!relationship.name) { + accept('error', 'Attribute missing id field', { node: relationship, property: 'name' }); + } + } } diff --git a/extensions/crossmodel-lang/src/language-server/cross-model.langium b/extensions/crossmodel-lang/src/language-server/cross-model.langium index cbe368d8..46fbd291 100644 --- a/extensions/crossmodel-lang/src/language-server/cross-model.langium +++ b/extensions/crossmodel-lang/src/language-server/cross-model.langium @@ -1,66 +1,141 @@ grammar CrossModel entry CrossModelRoot: - (entity=Entity | relationship=Relationship | diagram=SystemDiagram); + (entity=Entity | + relationship=Relationship | + diagram=SystemDiagram)?; +// Entity definition Entity: - 'entity' name=ID '{' - 'description' ':=' description=STRING ';' - ('attributes' '{' (attributes+=Attribute + ';')* '}' )? - '}' - ; + 'entity' ':' + ( + INDENT + EntityFields* + DEDENT + )* +; -Attribute: - name=ID ':=' value=(STRING | NUMBER); +EntityFields infers Entity: + ( + 'id' ':' name=STRING | + 'description' ':' description=STRING | + 'attributes' ':' EntityAttributes + ) +; +EntityAttributes infers Entity: + INDENT + (attributes+=EntityAttribute)* + DEDENT; + +EntityAttribute: + '-' EntityAttributeFields* +; + +EntityAttributeFields infers EntityAttribute: + ( + 'id' ':' name=STRING | + 'name' ':' name_val=STRING | + 'datatype' ':' datatype=STRING | + 'description' ':' description=STRING + ); + +// Relationship defintion Relationship: - 'relationship' name=ID '{' - 'source' ':=' source=[Entity:QualifiedName] ('with' sourceAttribute=[Attribute:QualifiedName])? ';' - 'target' ':=' target=[Entity:QualifiedName] ('with' targetAttribute=[Attribute:QualifiedName])? ';' - 'type' ':=' type=RelationshipType ';' - ('properties' '{' (properties+=Property + ';')* '}' )? - '}' - ; + 'relationship' ':' + ( + INDENT + RelationshipFields* + DEDENT + )* +; -Property: - key=ID ':=' value=(STRING | NUMBER); +RelationshipFields infers Relationship: + ( + 'id' ':' name=STRING | + 'name' ':' name_val=STRING | + 'description' ':' description=STRING | + 'parent' ':' parent=[Entity:QualifiedName] | + 'child' ':' child=[Entity:QualifiedName] | + 'type' ':' type=RelationshipType + ) +; RelationshipType returns string: - '1:1' | '1:n' | 'n:1' | 'n:m'; - + '1:1' | '1:n' | 'n:1' | 'n:m' +; +// Diagram defintion SystemDiagram: - 'diagram' '{' - (nodes+=DiagramNode ';')* - (edges+=DiagramEdge ';')* - '}' - ; + 'diagram' ':' + ( + INDENT + SystemDiagramFields* + DEDENT + )* +; + +SystemDiagramFields infers SystemDiagram: + ( + 'nodes' ':' SystemDiagramNodes | + 'edges' ':' SystemDiagramEdge | + 'id' ':' name=STRING | + 'description' ':' description=STRING + ) +; + +SystemDiagramNodes infers SystemDiagram: + INDENT + (nodes+=DiagramNode)* + DEDENT +; DiagramNode: - 'node' name=ID 'for' semanticElement=[Entity:QualifiedName] '{' - 'x' ':=' x=NUMBER ';' - 'y' ':=' y=NUMBER ';' - 'width' ':=' width=NUMBER ';' - 'height' ':=' height=NUMBER ';' - '}' - ; + '-' DiagramNodeFields* +; + +DiagramNodeFields infers DiagramNode: + 'id' ':' name=STRING | + 'for' ':' for=[Entity:QualifiedName] | + 'x' ':' x=NUMBER | + 'y' ':' y=NUMBER | + 'width' ':' width=NUMBER | + 'height' ':' height=NUMBER | + 'name' ':' name_val=STRING | + 'description' ':' description=STRING +; + +SystemDiagramEdge infers SystemDiagram: + INDENT + (edges+=DiagramEdge)* + DEDENT +; DiagramEdge: - 'edge' name=ID 'for' semanticElement=[Relationship:QualifiedName] '{' - 'source' ':=' source=[DiagramNode:QualifiedName] ';' - 'target' ':=' target=[DiagramNode:QualifiedName] ';' - // for non-straight connections we also need to save routing points - '}' - ; + '-' DiagramEdgeFields* +; +DiagramEdgeFields infers DiagramEdge: + ( + 'for' ':' for=[Relationship:QualifiedName] | + 'id' ':' name=STRING + ) +; +// Identification QualifiedName returns string: - ID ('.' ID)*; + STRING ('.' STRING)*; -hidden terminal WS: /\s+/; -terminal ID: /[_a-zA-Z@][\w_\-@\/#]*/; +// Scalar values terminal STRING: /"[^"]*"|'[^']*'/; terminal NUMBER returns number: /(-)?[0-9]+(\.[0-9]*)?/; -hidden terminal ML_COMMENT: /\/\*[\s\S]*?\*\//; -hidden terminal SL_COMMENT: /\/\/[^\n\r]*/; +// Misc +hidden terminal SL_COMMENT: /#[^\n\r]*/; + +// Terminals to get the indentation working +hidden terminal NEWLINE: 'this_string_does_not_matter_newline#$%^&*(('; +terminal DEDENT: 'this_string_does_not_matter_dedent#$%^&*(('; +terminal INDENT: 'this_string_does_not_matter_indent#$%^&*(('; +hidden terminal SPACES: 'this_string_does_not_matter_spaces#$%^&*(('; + diff --git a/extensions/crossmodel-lang/src/language-server/generated/ast.ts b/extensions/crossmodel-lang/src/language-server/generated/ast.ts index d9abae40..82beaf3b 100644 --- a/extensions/crossmodel-lang/src/language-server/generated/ast.ts +++ b/extensions/crossmodel-lang/src/language-server/generated/ast.ts @@ -10,19 +10,6 @@ export type QualifiedName = string; export type RelationshipType = '1:1' | '1:n' | 'n:1' | 'n:m'; -export interface Attribute extends AstNode { - readonly $container: Entity; - readonly $type: 'Attribute'; - name: string - value: number | string -} - -export const Attribute = 'Attribute'; - -export function isAttribute(item: unknown): item is Attribute { - return reflection.isInstance(item, Attribute); -} - export interface CrossModelRoot extends AstNode { readonly $type: 'CrossModelRoot'; diagram?: SystemDiagram @@ -39,10 +26,8 @@ export function isCrossModelRoot(item: unknown): item is CrossModelRoot { export interface DiagramEdge extends AstNode { readonly $container: SystemDiagram; readonly $type: 'DiagramEdge'; - name: string - semanticElement: Reference - source: Reference - target: Reference + for?: Reference + name?: string } export const DiagramEdge = 'DiagramEdge'; @@ -54,12 +39,14 @@ export function isDiagramEdge(item: unknown): item is DiagramEdge { export interface DiagramNode extends AstNode { readonly $container: SystemDiagram; readonly $type: 'DiagramNode'; - height: number - name: string - semanticElement: Reference - width: number - x: number - y: number + description?: string + for?: Reference + height?: number + name?: string + name_val?: string + width?: number + x?: number + y?: number } export const DiagramNode = 'DiagramNode'; @@ -71,9 +58,9 @@ export function isDiagramNode(item: unknown): item is DiagramNode { export interface Entity extends AstNode { readonly $container: CrossModelRoot; readonly $type: 'Entity'; - attributes: Array - description: string - name: string + attributes: Array + description?: string + name?: string } export const Entity = 'Entity'; @@ -82,29 +69,30 @@ export function isEntity(item: unknown): item is Entity { return reflection.isInstance(item, Entity); } -export interface Property extends AstNode { - readonly $container: Relationship; - readonly $type: 'Property'; - key: string - value: number | string +export interface EntityAttribute extends AstNode { + readonly $container: Entity; + readonly $type: 'EntityAttribute'; + datatype?: string + description?: string + name?: string + name_val?: string } -export const Property = 'Property'; +export const EntityAttribute = 'EntityAttribute'; -export function isProperty(item: unknown): item is Property { - return reflection.isInstance(item, Property); +export function isEntityAttribute(item: unknown): item is EntityAttribute { + return reflection.isInstance(item, EntityAttribute); } export interface Relationship extends AstNode { readonly $container: CrossModelRoot; readonly $type: 'Relationship'; - name: string - properties: Array - source: Reference - sourceAttribute?: Reference - target: Reference - targetAttribute?: Reference - type: RelationshipType + child?: Reference + description?: string + name?: string + name_val?: string + parent?: Reference + type?: RelationshipType } export const Relationship = 'Relationship'; @@ -116,7 +104,9 @@ export function isRelationship(item: unknown): item is Relationship { export interface SystemDiagram extends AstNode { readonly $container: CrossModelRoot; readonly $type: 'SystemDiagram'; + description?: string edges: Array + name?: string nodes: Array } @@ -127,12 +117,11 @@ export function isSystemDiagram(item: unknown): item is SystemDiagram { } export interface CrossModelAstType { - Attribute: Attribute CrossModelRoot: CrossModelRoot DiagramEdge: DiagramEdge DiagramNode: DiagramNode Entity: Entity - Property: Property + EntityAttribute: EntityAttribute Relationship: Relationship SystemDiagram: SystemDiagram } @@ -140,7 +129,7 @@ export interface CrossModelAstType { export class CrossModelAstReflection extends AbstractAstReflection { getAllTypes(): string[] { - return ['Attribute', 'CrossModelRoot', 'DiagramEdge', 'DiagramNode', 'Entity', 'Property', 'Relationship', 'SystemDiagram']; + return ['CrossModelRoot', 'DiagramEdge', 'DiagramNode', 'Entity', 'EntityAttribute', 'Relationship', 'SystemDiagram']; } protected override computeIsSubtype(subtype: string, supertype: string): boolean { @@ -154,22 +143,14 @@ export class CrossModelAstReflection extends AbstractAstReflection { getReferenceType(refInfo: ReferenceInfo): string { const referenceId = `${refInfo.container.$type}:${refInfo.property}`; switch (referenceId) { - case 'DiagramEdge:semanticElement': { + case 'DiagramEdge:for': { return Relationship; } - case 'DiagramEdge:source': - case 'DiagramEdge:target': { - return DiagramNode; - } - case 'DiagramNode:semanticElement': - case 'Relationship:source': - case 'Relationship:target': { + case 'DiagramNode:for': + case 'Relationship:child': + case 'Relationship:parent': { return Entity; } - case 'Relationship:sourceAttribute': - case 'Relationship:targetAttribute': { - return Attribute; - } default: { throw new Error(`${referenceId} is not a valid reference id.`); } @@ -186,14 +167,6 @@ export class CrossModelAstReflection extends AbstractAstReflection { ] }; } - case 'Relationship': { - return { - name: 'Relationship', - mandatory: [ - { name: 'properties', type: 'array' } - ] - }; - } case 'SystemDiagram': { return { name: 'SystemDiagram', diff --git a/extensions/crossmodel-lang/src/language-server/generated/grammar.ts b/extensions/crossmodel-lang/src/language-server/generated/grammar.ts index 21a70c83..7ef401fe 100644 --- a/extensions/crossmodel-lang/src/language-server/generated/grammar.ts +++ b/extensions/crossmodel-lang/src/language-server/generated/grammar.ts @@ -37,7 +37,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@3" + "$ref": "#/rules@6" }, "arguments": [] } @@ -49,12 +49,13 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@6" + "$ref": "#/rules@9" }, "arguments": [] } } - ] + ], + "cardinality": "?" }, "definesHiddenTokens": false, "fragment": false, @@ -72,90 +73,37 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "$type": "Keyword", "value": "entity" }, - { - "$type": "Assignment", - "feature": "name", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@11" - }, - "arguments": [] - } - }, - { - "$type": "Keyword", - "value": "{" - }, - { - "$type": "Keyword", - "value": "description" - }, - { - "$type": "Keyword", - "value": ":=" - }, - { - "$type": "Assignment", - "feature": "description", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@12" - }, - "arguments": [] - } - }, { "$type": "Keyword", - "value": ";" + "value": ":" }, { "$type": "Group", "elements": [ { - "$type": "Keyword", - "value": "attributes" - }, - { - "$type": "Keyword", - "value": "{" + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] }, { - "$type": "Group", - "elements": [ - { - "$type": "Assignment", - "feature": "attributes", - "operator": "+=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@2" - }, - "arguments": [] - }, - "cardinality": "+" - }, - { - "$type": "Keyword", - "value": ";" - } - ], + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@2" + }, + "arguments": [], "cardinality": "*" }, { - "$type": "Keyword", - "value": "}" + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] } ], - "cardinality": "?" - }, - { - "$type": "Keyword", - "value": "}" + "cardinality": "*" } ] }, @@ -168,49 +116,83 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load }, { "$type": "ParserRule", - "name": "Attribute", + "name": "EntityFields", + "inferredType": { + "$type": "InferredType", + "name": "Entity" + }, "definition": { - "$type": "Group", + "$type": "Alternatives", "elements": [ { - "$type": "Assignment", - "feature": "name", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@11" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "id" }, - "arguments": [] - } - }, - { - "$type": "Keyword", - "value": ":=" - }, - { - "$type": "Assignment", - "feature": "value", - "operator": "=", - "terminal": { - "$type": "Alternatives", - "elements": [ - { + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@12" + "$ref": "#/rules@18" }, "arguments": [] - }, - { + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "description" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "description", + "operator": "=", + "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@13" + "$ref": "#/rules@18" }, "arguments": [] } - ] - } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "attributes" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@3" + }, + "arguments": [] + } + ] } ] }, @@ -223,218 +205,237 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load }, { "$type": "ParserRule", - "name": "Relationship", + "name": "EntityAttributes", + "inferredType": { + "$type": "InferredType", + "name": "Entity" + }, "definition": { "$type": "Group", "elements": [ { - "$type": "Keyword", - "value": "relationship" + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] }, { "$type": "Assignment", - "feature": "name", - "operator": "=", + "feature": "attributes", + "operator": "+=", "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@11" + "$ref": "#/rules@4" }, "arguments": [] - } - }, - { - "$type": "Keyword", - "value": "{" + }, + "cardinality": "*" }, { - "$type": "Keyword", - "value": "source" - }, + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "EntityAttribute", + "definition": { + "$type": "Group", + "elements": [ { "$type": "Keyword", - "value": ":=" + "value": "-" }, { - "$type": "Assignment", - "feature": "source", - "operator": "=", - "terminal": { - "$type": "CrossReference", - "type": { - "$ref": "#/rules@1" - }, - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@9" - }, - "arguments": [] - }, - "deprecatedSyntax": false - } - }, + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@5" + }, + "arguments": [], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "EntityAttributeFields", + "inferredType": { + "$type": "InferredType", + "name": "EntityAttribute" + }, + "definition": { + "$type": "Alternatives", + "elements": [ { "$type": "Group", "elements": [ { "$type": "Keyword", - "value": "with" + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" }, { "$type": "Assignment", - "feature": "sourceAttribute", + "feature": "name", "operator": "=", "terminal": { - "$type": "CrossReference", - "type": { - "$ref": "#/rules@2" - }, - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@9" - }, - "arguments": [] + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" }, - "deprecatedSyntax": false + "arguments": [] } } - ], - "cardinality": "?" - }, - { - "$type": "Keyword", - "value": ";" + ] }, { - "$type": "Keyword", - "value": "target" - }, - { - "$type": "Keyword", - "value": ":=" - }, - { - "$type": "Assignment", - "feature": "target", - "operator": "=", - "terminal": { - "$type": "CrossReference", - "type": { - "$ref": "#/rules@1" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "name" }, - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@9" - }, - "arguments": [] + { + "$type": "Keyword", + "value": ":" }, - "deprecatedSyntax": false - } + { + "$type": "Assignment", + "feature": "name_val", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] }, { "$type": "Group", "elements": [ { "$type": "Keyword", - "value": "with" + "value": "datatype" + }, + { + "$type": "Keyword", + "value": ":" }, { "$type": "Assignment", - "feature": "targetAttribute", + "feature": "datatype", "operator": "=", "terminal": { - "$type": "CrossReference", - "type": { - "$ref": "#/rules@2" - }, - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@9" - }, - "arguments": [] + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" }, - "deprecatedSyntax": false + "arguments": [] } } - ], - "cardinality": "?" + ] }, { - "$type": "Keyword", - "value": ";" - }, - { - "$type": "Keyword", - "value": "type" - }, + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "description" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "description", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "Relationship", + "definition": { + "$type": "Group", + "elements": [ { "$type": "Keyword", - "value": ":=" - }, - { - "$type": "Assignment", - "feature": "type", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@5" - }, - "arguments": [] - } + "value": "relationship" }, { "$type": "Keyword", - "value": ";" + "value": ":" }, { "$type": "Group", "elements": [ { - "$type": "Keyword", - "value": "properties" - }, - { - "$type": "Keyword", - "value": "{" + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] }, { - "$type": "Group", - "elements": [ - { - "$type": "Assignment", - "feature": "properties", - "operator": "+=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@4" - }, - "arguments": [] - }, - "cardinality": "+" - }, - { - "$type": "Keyword", - "value": ";" - } - ], + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@7" + }, + "arguments": [], "cardinality": "*" }, { - "$type": "Keyword", - "value": "}" + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] } ], - "cardinality": "?" - }, - { - "$type": "Keyword", - "value": "}" + "cardinality": "*" } ] }, @@ -447,49 +448,177 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load }, { "$type": "ParserRule", - "name": "Property", + "name": "RelationshipFields", + "inferredType": { + "$type": "InferredType", + "name": "Relationship" + }, "definition": { - "$type": "Group", + "$type": "Alternatives", "elements": [ { - "$type": "Assignment", - "feature": "key", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@11" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "id" }, - "arguments": [] - } + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] }, { - "$type": "Keyword", - "value": ":=" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "name" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name_val", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] }, { - "$type": "Assignment", - "feature": "value", - "operator": "=", - "terminal": { - "$type": "Alternatives", - "elements": [ - { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "description" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "description", + "operator": "=", + "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@12" + "$ref": "#/rules@18" }, "arguments": [] - }, - { + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "parent" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "parent", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@1" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@17" + }, + "arguments": [] + }, + "deprecatedSyntax": false + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "child" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "child", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@1" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@17" + }, + "arguments": [] + }, + "deprecatedSyntax": false + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "type" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "type", + "operator": "=", + "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@13" + "$ref": "#/rules@8" }, "arguments": [] } - ] - } + } + ] } ] }, @@ -544,55 +673,144 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load }, { "$type": "Keyword", - "value": "{" + "value": ":" }, { "$type": "Group", "elements": [ { - "$type": "Assignment", - "feature": "nodes", - "operator": "+=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@7" - }, - "arguments": [] - } + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] }, { - "$type": "Keyword", - "value": ";" + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@10" + }, + "arguments": [], + "cardinality": "*" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] } ], "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "SystemDiagramFields", + "inferredType": { + "$type": "InferredType", + "name": "SystemDiagram" + }, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "nodes" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@11" + }, + "arguments": [] + } + ] }, { "$type": "Group", "elements": [ + { + "$type": "Keyword", + "value": "edges" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@14" + }, + "arguments": [] + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" + }, { "$type": "Assignment", - "feature": "edges", - "operator": "+=", + "feature": "name", + "operator": "=", "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@8" + "$ref": "#/rules@18" }, "arguments": [] } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "description" }, { "$type": "Keyword", - "value": ";" + "value": ":" + }, + { + "$type": "Assignment", + "feature": "description", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } } - ], - "cardinality": "*" - }, - { - "$type": "Keyword", - "value": "}" + ] } ] }, @@ -605,152 +823,293 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load }, { "$type": "ParserRule", - "name": "DiagramNode", + "name": "SystemDiagramNodes", + "inferredType": { + "$type": "InferredType", + "name": "SystemDiagram" + }, "definition": { "$type": "Group", "elements": [ { - "$type": "Keyword", - "value": "node" + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] }, { "$type": "Assignment", - "feature": "name", - "operator": "=", + "feature": "nodes", + "operator": "+=", "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@11" + "$ref": "#/rules@12" }, "arguments": [] - } - }, - { - "$type": "Keyword", - "value": "for" - }, - { - "$type": "Assignment", - "feature": "semanticElement", - "operator": "=", - "terminal": { - "$type": "CrossReference", - "type": { - "$ref": "#/rules@1" - }, - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@9" - }, - "arguments": [] - }, - "deprecatedSyntax": false - } + }, + "cardinality": "*" }, { - "$type": "Keyword", - "value": "{" - }, + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "DiagramNode", + "definition": { + "$type": "Group", + "elements": [ { "$type": "Keyword", - "value": "x" + "value": "-" }, { - "$type": "Keyword", - "value": ":=" - }, + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@13" + }, + "arguments": [], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "DiagramNodeFields", + "inferredType": { + "$type": "InferredType", + "name": "DiagramNode" + }, + "definition": { + "$type": "Alternatives", + "elements": [ { - "$type": "Assignment", - "feature": "x", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@13" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "id" }, - "arguments": [] - } - }, - { - "$type": "Keyword", - "value": ";" - }, - { - "$type": "Keyword", - "value": "y" - }, - { - "$type": "Keyword", - "value": ":=" - }, - { - "$type": "Assignment", - "feature": "y", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@13" + { + "$type": "Keyword", + "value": ":" }, - "arguments": [] - } - }, - { - "$type": "Keyword", - "value": ";" - }, - { - "$type": "Keyword", - "value": "width" - }, - { - "$type": "Keyword", - "value": ":=" + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] }, { - "$type": "Assignment", - "feature": "width", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@13" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "for" }, - "arguments": [] - } + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "for", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@1" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@17" + }, + "arguments": [] + }, + "deprecatedSyntax": false + } + } + ] }, { - "$type": "Keyword", - "value": ";" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "x" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "x", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@19" + }, + "arguments": [] + } + } + ] }, { - "$type": "Keyword", - "value": "height" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "y" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "y", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@19" + }, + "arguments": [] + } + } + ] }, { - "$type": "Keyword", - "value": ":=" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "width" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "width", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@19" + }, + "arguments": [] + } + } + ] }, { - "$type": "Assignment", - "feature": "height", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@13" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "height" }, - "arguments": [] - } + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "height", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@19" + }, + "arguments": [] + } + } + ] }, { - "$type": "Keyword", - "value": ";" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "name" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name_val", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] }, { - "$type": "Keyword", - "value": "}" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "description" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "description", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] } ] }, @@ -763,118 +1122,143 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load }, { "$type": "ParserRule", - "name": "DiagramEdge", + "name": "SystemDiagramEdge", + "inferredType": { + "$type": "InferredType", + "name": "SystemDiagram" + }, "definition": { "$type": "Group", "elements": [ { - "$type": "Keyword", - "value": "edge" + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] }, { "$type": "Assignment", - "feature": "name", - "operator": "=", + "feature": "edges", + "operator": "+=", "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@11" + "$ref": "#/rules@15" }, "arguments": [] - } - }, - { - "$type": "Keyword", - "value": "for" - }, - { - "$type": "Assignment", - "feature": "semanticElement", - "operator": "=", - "terminal": { - "$type": "CrossReference", - "type": { - "$ref": "#/rules@3" - }, - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@9" - }, - "arguments": [] - }, - "deprecatedSyntax": false - } + }, + "cardinality": "*" }, { - "$type": "Keyword", - "value": "{" - }, + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "DiagramEdge", + "definition": { + "$type": "Group", + "elements": [ { "$type": "Keyword", - "value": "source" + "value": "-" }, { - "$type": "Keyword", - "value": ":=" - }, + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@16" + }, + "arguments": [], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "DiagramEdgeFields", + "inferredType": { + "$type": "InferredType", + "name": "DiagramEdge" + }, + "definition": { + "$type": "Alternatives", + "elements": [ { - "$type": "Assignment", - "feature": "source", - "operator": "=", - "terminal": { - "$type": "CrossReference", - "type": { - "$ref": "#/rules@7" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "for" }, - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@9" - }, - "arguments": [] + { + "$type": "Keyword", + "value": ":" }, - "deprecatedSyntax": false - } - }, - { - "$type": "Keyword", - "value": ";" - }, - { - "$type": "Keyword", - "value": "target" - }, - { - "$type": "Keyword", - "value": ":=" + { + "$type": "Assignment", + "feature": "for", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@6" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@17" + }, + "arguments": [] + }, + "deprecatedSyntax": false + } + } + ] }, { - "$type": "Assignment", - "feature": "target", - "operator": "=", - "terminal": { - "$type": "CrossReference", - "type": { - "$ref": "#/rules@7" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "id" }, - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@9" - }, - "arguments": [] + { + "$type": "Keyword", + "value": ":" }, - "deprecatedSyntax": false - } - }, - { - "$type": "Keyword", - "value": ";" - }, - { - "$type": "Keyword", - "value": "}" + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] } ] }, @@ -895,7 +1279,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@11" + "$ref": "#/rules@18" }, "arguments": [] }, @@ -909,7 +1293,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@11" + "$ref": "#/rules@18" }, "arguments": [] } @@ -927,65 +1311,87 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load }, { "$type": "TerminalRule", - "hidden": true, - "name": "WS", + "name": "STRING", "definition": { "$type": "RegexToken", - "regex": "\\\\s+" + "regex": "\\"[^\\"]*\\"|'[^']*'" }, - "fragment": false + "fragment": false, + "hidden": false }, { "$type": "TerminalRule", - "name": "ID", + "name": "NUMBER", + "type": { + "$type": "ReturnType", + "name": "number" + }, "definition": { "$type": "RegexToken", - "regex": "[_a-zA-Z@][\\\\w_\\\\-@\\\\/#]*" + "regex": "(-)?[0-9]+(\\\\.[0-9]*)?" }, "fragment": false, "hidden": false }, { "$type": "TerminalRule", - "name": "STRING", + "hidden": true, + "name": "SL_COMMENT", "definition": { "$type": "RegexToken", - "regex": "\\"[^\\"]*\\"|'[^']*'" + "regex": "#[^\\\\n\\\\r]*" }, - "fragment": false, - "hidden": false + "fragment": false }, { "$type": "TerminalRule", - "name": "NUMBER", - "type": { - "$type": "ReturnType", - "name": "number" + "hidden": true, + "name": "NEWLINE", + "definition": { + "$type": "CharacterRange", + "left": { + "$type": "Keyword", + "value": "this_string_does_not_matter_newline#$%^&*((" + } }, + "fragment": false + }, + { + "$type": "TerminalRule", + "name": "DEDENT", "definition": { - "$type": "RegexToken", - "regex": "(-)?[0-9]+(\\\\.[0-9]*)?" + "$type": "CharacterRange", + "left": { + "$type": "Keyword", + "value": "this_string_does_not_matter_dedent#$%^&*((" + } }, "fragment": false, "hidden": false }, { "$type": "TerminalRule", - "hidden": true, - "name": "ML_COMMENT", + "name": "INDENT", "definition": { - "$type": "RegexToken", - "regex": "\\\\/\\\\*[\\\\s\\\\S]*?\\\\*\\\\/" + "$type": "CharacterRange", + "left": { + "$type": "Keyword", + "value": "this_string_does_not_matter_indent#$%^&*((" + } }, - "fragment": false + "fragment": false, + "hidden": false }, { "$type": "TerminalRule", "hidden": true, - "name": "SL_COMMENT", + "name": "SPACES", "definition": { - "$type": "RegexToken", - "regex": "\\\\/\\\\/[^\\\\n\\\\r]*" + "$type": "CharacterRange", + "left": { + "$type": "Keyword", + "value": "this_string_does_not_matter_spaces#$%^&*((" + } }, "fragment": false } diff --git a/extensions/crossmodel-lang/src/language-server/lexer/cross-model-indent-stack.ts b/extensions/crossmodel-lang/src/language-server/lexer/cross-model-indent-stack.ts new file mode 100644 index 00000000..e61b763d --- /dev/null +++ b/extensions/crossmodel-lang/src/language-server/lexer/cross-model-indent-stack.ts @@ -0,0 +1,81 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +import _ from 'lodash'; +import { IndentStackError } from './cross-model-lexer-error'; + +/** + * Class to hold the current indentation levels. Has a few basic functions to handle + * basic operations. + */ +class IndentStack { + /** + * Stack to store the indentation levels. + */ + private stack: number[] = [0]; + + /** + * Retrieves a copy of the current stack. + * @returns {number[]} A copy of the current stack. + */ + public get(): number[] { + return [...this.stack]; + } + + /** + * Pushes a new indentation level onto the stack. + * @param {number} value - The indentation level to push. + */ + public push(value: number): void { + this.stack.push(value); + } + + /** + * Resets the stack to contain only the initial indentation level. + */ + public reset(): void { + this.stack = [0]; + } + + /** + * Removes and returns the topmost indentation level from the stack. + * @returns {number | undefined} The popped indentation level, or undefined if the stack is empty. + */ + public pop(): number | undefined { + return this.stack.pop(); + } + + /** + * Returns the length of indentation levels in the stack. + * @returns {number} The length of the stack. + */ + public length(): number { + return this.stack.length; + } + + /** + * Retrieves the last indentation level from the stack. + * @returns {number} The last indentation level. + * @throws {Error} If the stack is empty. + */ + public getLast(): number { + const lastValue = _.last(this.stack); + + if (lastValue === undefined) { + throw new IndentStackError('Indent stack is empty.'); + } + + return lastValue; + } + + /** + * Finds the last index of the specified indentation level in the stack. + * @param {number} value - The indentation level to search for. + * @returns {number} The index of the last occurrence of the indentation level, or -1 if not found. + */ + public findLastIndex(value: number): number { + return this.stack.lastIndexOf(value); + } +} + +export const indentStack = new IndentStack(); diff --git a/extensions/crossmodel-lang/src/language-server/lexer/cross-model-indentation-tokens.ts b/extensions/crossmodel-lang/src/language-server/lexer/cross-model-indentation-tokens.ts new file mode 100644 index 00000000..45a029c2 --- /dev/null +++ b/extensions/crossmodel-lang/src/language-server/lexer/cross-model-indentation-tokens.ts @@ -0,0 +1,140 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +/* eslint-disable no-null/no-null */ + +import { IToken, Lexer, createToken, createTokenInstance } from 'chevrotain'; +import _ from 'lodash'; +import { indentStack } from './cross-model-indent-stack'; +import { IndentationError } from './cross-model-lexer-error'; + +export const NAMES = { + INDENT: 'INDENT', + DEDENT: 'DEDENT', + SPACES: 'SPACES', + NEWLINE: 'NEWLINE' +}; + +export const NEWLINE = createToken({ + name: NAMES.NEWLINE, + pattern: /\n|\r\n?/, + group: 'nl' +}); + +// Spaces that are not an indent or dedent should be ignored +// Done by giving the group Lexer.SKIPPED +export const SPACES = createToken({ + name: NAMES.SPACES, + pattern: / +/, + group: Lexer.SKIPPED +}); + +export const INDENT = createToken({ + name: NAMES.INDENT, + pattern: _.partialRight(matchIndentBase, 'indent'), + // custom token patterns should explicitly specify the line_breaks option + line_breaks: false +}); + +export const DEDENT = createToken({ + name: NAMES.DEDENT, + pattern: _.partialRight(matchIndentBase, 'dedent'), + // custom token patterns should explicitly specify the line_breaks option + line_breaks: false +}); + +/** + * Indentation/dedenentation tokens based on rules and returns the matched token. + * + * @param text The input text being analyzed. + * @param offset The current offset in the input text. + * @param matchedTokens An array of tokens that have been matched so far. + * @param groups An object containing groups of tokens matched so far. + * @param type The type of indentation to check for ("indent" or "dedent"). + * @returns The matched indent or dedent token. null if no indentation match is found. + * @throws {IndentationError} If an invalid outdent is encountered. + */ +function matchIndentBase(text: string, offset: number, matchedTokens: IToken[], groups: any, type: string) { + const noTokensMatchedYet = _.isEmpty(matchedTokens); + const newLines: Array = groups.nl; + const noNewLinesMatchedYet = _.isEmpty(newLines); + const isFirstLine = noTokensMatchedYet && noNewLinesMatchedYet; + const last_newline = _.last(newLines); + + // Windows line endings are \r\n, linux is only \n. This variable accounts for that. + const offset_match = /\r\n/.exec(last_newline?.image as string); + const offset_newline = offset_match ? 2 : 1; + + const isStartOfLine = + // only newlines matched so far + (noTokensMatchedYet && !noNewLinesMatchedYet) || + // Both newlines and other Tokens have been matched AND the offset is just after the last newline + (!noTokensMatchedYet && !noNewLinesMatchedYet && last_newline && offset === last_newline.startOffset + offset_newline); + + // indentation can only be matched at the start of a line. + if (isFirstLine || isStartOfLine) { + let currIndentLevel: number | undefined = undefined; + const prevIndentLevel = indentStack.getLast(); + + const wsRegExp = /[ ]+/y; + wsRegExp.lastIndex = offset; + const match = wsRegExp.exec(text); + + // possible non-empty indentation + if (match !== null) { + currIndentLevel = match[0].length; + + // To get the - working for the lists + const minusRegex = /-[ ]*/y; + minusRegex.lastIndex = match[0].length + offset; + const minusMatch = minusRegex.exec(text); + if (minusMatch) { + currIndentLevel = currIndentLevel + minusMatch[0].length; + } + } + // "empty" indentation means indentLevel of 0. + else { + currIndentLevel = 0; + } + + // deeper indentation + if (currIndentLevel > prevIndentLevel && type === 'indent') { + indentStack.push(currIndentLevel); + return match; + } + // shallower indentation + else if (currIndentLevel < prevIndentLevel && type === 'dedent') { + const matchIndentIndex = indentStack.findLastIndex(currIndentLevel); + + // any outdent must match some previous indentation level. + if (matchIndentIndex === -1) { + throw new IndentationError(`invalid outdent at offset: ${offset}`); + } + + const numberOfDedents = indentStack.length() - matchIndentIndex - 1; + + // This is a little tricky + // 1. If there is no match (0 level indent) than this custom token + // matcher would return "null" and so we need to add all the required outdents ourselves. + // 2. If there was match (> 0 level indent) than we need to add minus one number of outdents + // because the lexer would create one due to returning a none null result. + const iStart = match !== null ? 1 : 0; + for (let i = iStart; i < numberOfDedents; i++) { + indentStack.pop(); + matchedTokens.push(createTokenInstance(DEDENT, NAMES.DEDENT, offset, offset, newLines.length, newLines.length, 0, 0)); + } + + // even though we are adding fewer outdents directly we still need to update the indent stack fully. + if (iStart === 1) { + indentStack.pop(); + } + return match; + } else { + // same indent, this should be lexed as simple whitespace and ignored + return null; + } + } else { + // indentation cannot be matched under other circumstances + return null; + } +} diff --git a/extensions/crossmodel-lang/src/language-server/lexer/cross-model-lexer-error.ts b/extensions/crossmodel-lang/src/language-server/lexer/cross-model-lexer-error.ts new file mode 100644 index 00000000..258b4aca --- /dev/null +++ b/extensions/crossmodel-lang/src/language-server/lexer/cross-model-lexer-error.ts @@ -0,0 +1,14 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export class IndentationError extends Error { + constructor(message: string) { + super(message); + } +} + +export class IndentStackError extends Error { + constructor(message: string) { + super(message); + } +} diff --git a/extensions/crossmodel-lang/src/language-server/lexer/cross-model-lexer.ts b/extensions/crossmodel-lang/src/language-server/lexer/cross-model-lexer.ts new file mode 100644 index 00000000..1aafb78d --- /dev/null +++ b/extensions/crossmodel-lang/src/language-server/lexer/cross-model-lexer.ts @@ -0,0 +1,83 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ + +import { DefaultLexer, LexerResult } from 'langium'; +import { indentStack } from './cross-model-indent-stack'; +import { IndentationError } from './cross-model-lexer-error'; +import { createTokenInstance } from 'chevrotain'; +import { DEDENT, NAMES } from './cross-model-indentation-tokens'; + +/** + * Custom CrossModelLexer to get indentation working. + */ +export class CrossModelLexer extends DefaultLexer { + /** + * Tokenize the given text. custom implementation to get indentation working. + * + * @param text The text to tokenize + * @returns LexerResult, The result of the lexing + */ + override tokenize(text: string): LexerResult { + indentStack.reset(); + let chevrotainResult; + + // In case there is a error lexing, Most of the time this is a lexer error but can + try { + chevrotainResult = super.tokenize(text); + } catch (error) { + const returnResult: LexerResult = { + tokens: [], + hidden: [], + errors: [] + }; + + if (error instanceof IndentationError) { + returnResult.errors.push({ + message: 'Indentation error: Make sure the indentation is correct in the file', + offset: 0, + line: 1, + column: 1, + length: 1 + }); + } else { + returnResult.errors.push({ + message: 'Unknown error Lexer error', + offset: 0, + line: 1, + column: 1, + length: 1 + }); + + throw error; + } + + return returnResult; + } + + // The lexer does not add trailing dedents at the end of the file + // this method does it for us + this.createTrailingDedentTokens(text, chevrotainResult); + + return chevrotainResult; + } + + /** + * Add dedents tokens at the end of the tokenlist. + * + * @param text The text that was tokenized + * @param lexingResult The token results of the text + */ + private createTrailingDedentTokens(text: string, lexingResult: LexerResult): void { + // These are there to put the error warning in the right place in the editor + const newlines = text.split(/\r\n|\r|\n/).length; + let match = text.match(/.*$/)?.length; + match = match ? match - 1 : 0; + + // add remaining Outdents + while (indentStack.length() > 1) { + lexingResult.tokens.push(createTokenInstance(DEDENT, NAMES.DEDENT, text.length, text.length, newlines, newlines, match, match)); + indentStack.pop(); + } + } +} diff --git a/extensions/crossmodel-lang/src/language-server/lexer/cross-model-token-generator.ts b/extensions/crossmodel-lang/src/language-server/lexer/cross-model-token-generator.ts new file mode 100644 index 00000000..5e68e8c8 --- /dev/null +++ b/extensions/crossmodel-lang/src/language-server/lexer/cross-model-token-generator.ts @@ -0,0 +1,84 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ + +import { DefaultTokenBuilder, TokenBuilderOptions } from 'langium'; +import { Grammar, TerminalRule } from 'langium/lib/grammar/generated/ast'; +import { TokenType, TokenVocabulary } from 'chevrotain'; +import { DEDENT, INDENT, NEWLINE, SPACES, NAMES } from './cross-model-indentation-tokens'; + +/** + * Custom implementation of TokenBuilder for the CrossModel language. + * Overrides the default behavior to handle custom indentation tokens. + */ +export class CrossModelTokenBuilder extends DefaultTokenBuilder { + /** + * Overrides the base implementation to handle custom indentation tokens. + * + * Makes use of the overridden method but shifts around the given tokens to make + * it working with indentation. + * + * @param grammar The grammar of the language. + * @param options + * @returns The token vocabulary for the language. + * @throws Error if any of the required custom indentation tokens (SPACES, INDENT, DEDENT, NEWLINE) is missing in the grammar. + */ + override buildTokens(grammar: Grammar, options?: TokenBuilderOptions): TokenVocabulary { + const tokens: TokenType[] = super.buildTokens(grammar, options) as TokenType[]; + + const updatedTokens: TokenType[] = []; + let tokenWithSpaces: TokenType | undefined = undefined; + let tokenIndent: TokenType | undefined = undefined; + let tokenDedent: TokenType | undefined = undefined; + let tokenNewLine: TokenType | undefined = undefined; + + for (const token of tokens) { + if (token.name === NAMES.SPACES) { + tokenWithSpaces = token; + } else if (token.name === NAMES.DEDENT) { + tokenDedent = token; + } else if (token.name === NAMES.INDENT) { + tokenIndent = token; + } else if (token.name === NAMES.NEWLINE) { + tokenNewLine = token; + } else { + updatedTokens.push(token); + } + } + + if (!tokenWithSpaces || !tokenIndent || !tokenDedent || !tokenNewLine) { + throw new Error('Missing indentation, new line or spaces tokens in grammar'); + } + + updatedTokens.push(tokenWithSpaces); + updatedTokens.unshift(tokenNewLine, tokenDedent, tokenIndent); + + return updatedTokens; + } + + /** + * Build a terminal token for the given TerminalRule. + * Overrides the base implementation to handle custom indentation tokens. + * + * @param terminal The TerminalRule for which to build the token. + * @returns The TokenType representing the terminal token. + * + */ + protected override buildTerminalToken(terminal: TerminalRule): TokenType { + let token; + + if (terminal.name === NAMES.NEWLINE) { + token = NEWLINE; + } else if (terminal.name === NAMES.INDENT) { + token = INDENT; + } else if (terminal.name === NAMES.DEDENT) { + token = DEDENT; + } else if (terminal.name === NAMES.SPACES) { + token = SPACES; + } else { + token = super.buildTerminalToken(terminal); + } + + return token; + } +} diff --git a/extensions/crossmodel-lang/syntaxes/cross-model.tmLanguage.json b/extensions/crossmodel-lang/syntaxes/cross-model.tmLanguage.json index a8c982f0..21caaafa 100644 --- a/extensions/crossmodel-lang/syntaxes/cross-model.tmLanguage.json +++ b/extensions/crossmodel-lang/syntaxes/cross-model.tmLanguage.json @@ -10,7 +10,7 @@ }, { "name": "keyword.control.cross-model", - "match": "\\b(1:n|attributes|description|diagram|edge|entity|for|height|n:1|n:m|node|properties|relationship|source|target|type|width|with|x|y)\\b" + "match": "\\b(1:n|attributes|child|datatype|description|diagram|edges|entity|for|height|id|n:1|n:m|name|nodes|parent|relationship|type|width|x|y)\\b" }, { "name": "string.quoted.double.cross-model", @@ -36,15 +36,25 @@ "repository": { "comments": { "patterns": [ + { + "begin": "#", + "beginCaptures": { + "1": { + "name": "punctuation.whitespace.comment.leading.cross-model" + } + }, + "end": "(?=$)", + "name": "comment.line.cross-model" + }, { "name": "comment.block.cross-model", - "begin": "/\\*", + "begin": "this_string_does_not_matter_newline#\\$%\\^&\\*\\(\\(", "beginCaptures": { "0": { "name": "punctuation.definition.comment.cross-model" } }, - "end": "\\*/", + "end": "this_string_does_not_matter_newline#\\$%\\^&\\*\\(\\(", "endCaptures": { "0": { "name": "punctuation.definition.comment.cross-model" @@ -52,14 +62,19 @@ } }, { - "begin": "//", + "name": "comment.block.cross-model", + "begin": "this_string_does_not_matter_spaces#\\$%\\^&\\*\\(\\(", "beginCaptures": { - "1": { - "name": "punctuation.whitespace.comment.leading.cross-model" + "0": { + "name": "punctuation.definition.comment.cross-model" } }, - "end": "(?=$)", - "name": "comment.line.cross-model" + "end": "this_string_does_not_matter_spaces#\\$%\\^&\\*\\(\\(", + "endCaptures": { + "0": { + "name": "punctuation.definition.comment.cross-model" + } + } } ] }, From a40b2b8dfa6da72cdcb87ce0a5d184f5e2f72bf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96zcan=20Seker?= Date: Thu, 17 Aug 2023 11:18:23 +0200 Subject: [PATCH 03/22] Implemented testing for new language --- configs/jest.config.js | 5 + extensions/crossmodel-lang/jest.config.js | 5 + extensions/crossmodel-lang/package.json | 1 + .../src/language-server/cross-model.langium | 4 +- .../src/language-server/generated/ast.ts | 2 + .../src/language-server/generated/grammar.ts | 50 + .../cross-model-lang-diagram.test.ts | 133 ++ .../cross-model-lang-entity.test.ts | 79 + .../cross-model-lang-relationship.test.ts | 46 + .../lexer/cross-model-indent-stack.test.ts | 129 ++ .../cross-model-indentation-tokens.test.ts | 306 ++++ .../lexer/cross-model-lexer.test.ts | 95 ++ .../lexer/cross-model-token-generator.test.ts | 58 + .../utils/example-grammar-no-indent.ts | 1343 ++++++++++++++++ .../language-server/utils/example-grammar.ts | 1356 +++++++++++++++++ .../utils/test-documents/diagram/diagram1.ts | 5 + .../utils/test-documents/diagram/diagram2.ts | 12 + .../utils/test-documents/diagram/diagram3.ts | 8 + .../utils/test-documents/diagram/diagram4.ts | 5 + .../utils/test-documents/diagram/diagram5.ts | 17 + .../utils/test-documents/diagram/diagram6.ts | 17 + .../utils/test-documents/diagram/index.ts | 9 + .../utils/test-documents/entity/entity1.ts | 7 + .../utils/test-documents/entity/entity2.ts | 26 + .../utils/test-documents/entity/entity3.ts | 26 + .../utils/test-documents/entity/entity4.ts | 26 + .../utils/test-documents/entity/index.ts | 7 + .../test-documents/relationship/index.ts | 5 + .../relationship/relationship1.ts | 10 + .../relationship/relationship2.ts | 8 + .../test/language-server/utils/utils.ts | 26 + package.json | 6 +- tsconfig.eslint.json | 2 +- 33 files changed, 3829 insertions(+), 5 deletions(-) create mode 100644 configs/jest.config.js create mode 100644 extensions/crossmodel-lang/jest.config.js create mode 100644 extensions/crossmodel-lang/test/language-server/cross-model-lang-diagram.test.ts create mode 100644 extensions/crossmodel-lang/test/language-server/cross-model-lang-entity.test.ts create mode 100644 extensions/crossmodel-lang/test/language-server/cross-model-lang-relationship.test.ts create mode 100644 extensions/crossmodel-lang/test/language-server/lexer/cross-model-indent-stack.test.ts create mode 100644 extensions/crossmodel-lang/test/language-server/lexer/cross-model-indentation-tokens.test.ts create mode 100644 extensions/crossmodel-lang/test/language-server/lexer/cross-model-lexer.test.ts create mode 100644 extensions/crossmodel-lang/test/language-server/lexer/cross-model-token-generator.test.ts create mode 100644 extensions/crossmodel-lang/test/language-server/utils/example-grammar-no-indent.ts create mode 100644 extensions/crossmodel-lang/test/language-server/utils/example-grammar.ts create mode 100644 extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram1.ts create mode 100644 extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram2.ts create mode 100644 extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram3.ts create mode 100644 extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram4.ts create mode 100644 extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram5.ts create mode 100644 extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram6.ts create mode 100644 extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/index.ts create mode 100644 extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity1.ts create mode 100644 extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity2.ts create mode 100644 extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity3.ts create mode 100644 extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity4.ts create mode 100644 extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/index.ts create mode 100644 extensions/crossmodel-lang/test/language-server/utils/test-documents/relationship/index.ts create mode 100644 extensions/crossmodel-lang/test/language-server/utils/test-documents/relationship/relationship1.ts create mode 100644 extensions/crossmodel-lang/test/language-server/utils/test-documents/relationship/relationship2.ts create mode 100644 extensions/crossmodel-lang/test/language-server/utils/utils.ts diff --git a/configs/jest.config.js b/configs/jest.config.js new file mode 100644 index 00000000..257ddde2 --- /dev/null +++ b/configs/jest.config.js @@ -0,0 +1,5 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node' +}; diff --git a/extensions/crossmodel-lang/jest.config.js b/extensions/crossmodel-lang/jest.config.js new file mode 100644 index 00000000..18d7e3c1 --- /dev/null +++ b/extensions/crossmodel-lang/jest.config.js @@ -0,0 +1,5 @@ +const baseConfig = require('../../configs/jest.config'); + +module.exports = { + ...baseConfig +}; diff --git a/extensions/crossmodel-lang/package.json b/extensions/crossmodel-lang/package.json index 38e7e1a0..7d4e6884 100644 --- a/extensions/crossmodel-lang/package.json +++ b/extensions/crossmodel-lang/package.json @@ -41,6 +41,7 @@ "symlink": "yarn symlink:browser && yarn symlink:electron", "symlink:browser": "symlink-dir . ../../applications/browser-app/plugins/crossmodel-lang", "symlink:electron": "symlink-dir . ../../applications/electron-app/plugins/crossmodel-lang", + "test": "jest", "vscode:prepublish": "yarn lint", "watch": "yarn watch:webpack", "watch:tsc": "tsc -b tsconfig.json --watch", diff --git a/extensions/crossmodel-lang/src/language-server/cross-model.langium b/extensions/crossmodel-lang/src/language-server/cross-model.langium index 46fbd291..32d782b8 100644 --- a/extensions/crossmodel-lang/src/language-server/cross-model.langium +++ b/extensions/crossmodel-lang/src/language-server/cross-model.langium @@ -18,6 +18,7 @@ Entity: EntityFields infers Entity: ( 'id' ':' name=STRING | + 'name' ':' name_val=STRING | 'description' ':' description=STRING | 'attributes' ':' EntityAttributes ) @@ -80,7 +81,8 @@ SystemDiagramFields infers SystemDiagram: 'nodes' ':' SystemDiagramNodes | 'edges' ':' SystemDiagramEdge | 'id' ':' name=STRING | - 'description' ':' description=STRING + 'description' ':' description=STRING | + 'name' ':' name_val=STRING ) ; diff --git a/extensions/crossmodel-lang/src/language-server/generated/ast.ts b/extensions/crossmodel-lang/src/language-server/generated/ast.ts index 82beaf3b..64f8e69e 100644 --- a/extensions/crossmodel-lang/src/language-server/generated/ast.ts +++ b/extensions/crossmodel-lang/src/language-server/generated/ast.ts @@ -61,6 +61,7 @@ export interface Entity extends AstNode { attributes: Array description?: string name?: string + name_val?: string } export const Entity = 'Entity'; @@ -107,6 +108,7 @@ export interface SystemDiagram extends AstNode { description?: string edges: Array name?: string + name_val?: string nodes: Array } diff --git a/extensions/crossmodel-lang/src/language-server/generated/grammar.ts b/extensions/crossmodel-lang/src/language-server/generated/grammar.ts index 7ef401fe..4a670fb7 100644 --- a/extensions/crossmodel-lang/src/language-server/generated/grammar.ts +++ b/extensions/crossmodel-lang/src/language-server/generated/grammar.ts @@ -149,6 +149,31 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load } ] }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "name" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name_val", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, { "$type": "Group", "elements": [ @@ -811,6 +836,31 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load } } ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "name" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name_val", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] } ] }, diff --git a/extensions/crossmodel-lang/test/language-server/cross-model-lang-diagram.test.ts b/extensions/crossmodel-lang/test/language-server/cross-model-lang-diagram.test.ts new file mode 100644 index 00000000..ade1f67f --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/cross-model-lang-diagram.test.ts @@ -0,0 +1,133 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +import { describe, expect, test } from '@jest/globals'; +import { EmptyFileSystem, isReference } from 'langium'; + +import { parseDocument } from './utils/utils'; +import { diagram1, diagram2, diagram3, diagram4, diagram5, diagram6 } from './utils/test-documents/diagram/index'; + +import { CrossModelRoot } from '../../src/language-server/generated/ast'; +import { createCrossModelServices } from '../../src/language-server/cross-model-module'; + +const services = createCrossModelServices({ ...EmptyFileSystem }).CrossModel; + +describe('CrossModel language Diagram', () => { + describe('Diagram without nodes and edges', () => { + test('Simple file for diagram', async () => { + const document = diagram1; + const parsedDocument = await parseDocument(services, document); + const model = parsedDocument.parseResult.value as CrossModelRoot; + + expect(model).toHaveProperty('diagram'); + expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); + expect(parsedDocument.parseResult.parserErrors.length).toBe(0); + + expect(model.diagram?.name).toBe('Systemdiagram1'); + }); + + test('Diagram with indentation error', async () => { + const document = diagram4; + const parsedDocument = await parseDocument(services, document); + const model = parsedDocument.parseResult.value as CrossModelRoot; + + expect(model).toHaveProperty('diagram'); + expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); + expect(parsedDocument.parseResult.parserErrors.length).toBe(1); + }); + }); + + describe('Diagram with nodes', () => { + test('Simple file for diagram and nodes', async () => { + const document = diagram2; + const parsedDocument = await parseDocument(services, document); + const model = parsedDocument.parseResult.value as CrossModelRoot; + const node1 = model.diagram?.nodes[0]; + + expect(model).toHaveProperty('diagram'); + expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); + expect(parsedDocument.parseResult.parserErrors.length).toBe(0); + + expect(model.diagram?.nodes.length).toBe(1); + + expect(node1?.name).toBe('CustomerNode'); + expect(isReference(node1?.for)).toBe(true); + expect(node1?.for?.$refText).toBe('Customer'); + expect(node1?.x).toBe(100); + }); + }); + + describe('Diagram with edges', () => { + test('Simple file for diagram and edges', async () => { + const document = diagram3; + const parsedDocument = await parseDocument(services, document); + const model = parsedDocument.parseResult.value as CrossModelRoot; + const edge1 = model.diagram?.edges[0]; + + expect(model).toHaveProperty('diagram'); + expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); + expect(parsedDocument.parseResult.parserErrors.length).toBe(0); + + expect(model.diagram?.edges.length).toBe(1); + + expect(edge1?.name).toBe('OrderCustomerEdge'); + expect(isReference(edge1?.for)).toBe(true); + expect(edge1?.for?.$refText).toBe('Order_Customer'); + }); + }); + + describe('Diagram with nodes and edges', () => { + test('Simple file for diagram and edges', async () => { + const document = diagram5; + const parsedDocument = await parseDocument(services, document); + const model = parsedDocument.parseResult.value as CrossModelRoot; + const node1 = model.diagram?.nodes[0]; + const edge1 = model.diagram?.edges[0]; + + expect(model).toHaveProperty('diagram'); + + expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); + expect(parsedDocument.parseResult.parserErrors.length).toBe(0); + + expect(model.diagram?.name_val).toBe('System diagram 1'); + expect(model.diagram?.description).toBe('This is a basic diagram with nodes and edges'); + expect(model.diagram?.nodes.length).toBe(1); + expect(model.diagram?.edges.length).toBe(1); + + expect(node1?.name).toBe('CustomerNode'); + expect(isReference(node1?.for)).toBe(true); + expect(node1?.for?.$refText).toBe('Customer'); + expect(node1?.x).toBe(100); + + expect(edge1?.name).toBe('OrderCustomerEdge'); + expect(isReference(edge1?.for)).toBe(true); + expect(edge1?.for?.$refText).toBe('Order_Customer'); + }); + + test('Simple file for diagram and edges, but descirption and name coming last', async () => { + const document = diagram6; + const parsedDocument = await parseDocument(services, document); + const model = parsedDocument.parseResult.value as CrossModelRoot; + const node1 = model.diagram?.nodes[0]; + const edge1 = model.diagram?.edges[0]; + + expect(model).toHaveProperty('diagram'); + expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); + expect(parsedDocument.parseResult.parserErrors.length).toBe(0); + + expect(model.diagram?.name_val).toBe('System diagram 1'); + expect(model.diagram?.description).toBe('This is a basic diagram with nodes and edges'); + expect(model.diagram?.nodes.length).toBe(1); + expect(model.diagram?.edges.length).toBe(1); + + expect(node1?.name).toBe('CustomerNode'); + expect(isReference(node1?.for)).toBe(true); + expect(node1?.for?.$refText).toBe('Customer'); + expect(node1?.x).toBe(100); + + expect(edge1?.name).toBe('OrderCustomerEdge'); + expect(isReference(edge1?.for)).toBe(true); + expect(edge1?.for?.$refText).toBe('Order_Customer'); + }); + }); +}); diff --git a/extensions/crossmodel-lang/test/language-server/cross-model-lang-entity.test.ts b/extensions/crossmodel-lang/test/language-server/cross-model-lang-entity.test.ts new file mode 100644 index 00000000..137d30ea --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/cross-model-lang-entity.test.ts @@ -0,0 +1,79 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ + +import { describe, expect, test } from '@jest/globals'; +import { EmptyFileSystem } from 'langium'; + +import { parseDocument } from './utils/utils'; +import { entity1, entity2, entity3, entity4 } from './utils/test-documents/entity/index'; + +import { CrossModelRoot } from '../../src/language-server/generated/ast'; +import { createCrossModelServices } from '../../src/language-server/cross-model-module'; + +const services = createCrossModelServices({ ...EmptyFileSystem }).CrossModel; + +describe('CrossModel language Entity', () => { + describe('Without attributes', () => { + test('Simple file for entity', async () => { + const document = entity1; + const parsedDocument = await parseDocument(services, document); + const model = parsedDocument.parseResult.value as CrossModelRoot; + + expect(model).toHaveProperty('entity'); + expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); + expect(parsedDocument.parseResult.parserErrors.length).toBe(0); + + expect(model.entity?.name).toBe('Customer'); + expect(model.entity?.name_val).toBe('Customer'); + expect(model.entity?.description).toBe('A customer with whom a transaction has been made.'); + }); + }); + + describe('With attributes', () => { + test('entity with attributes', async () => { + const document = entity2; + const parsedDocument = await parseDocument(services, document); + const model = parsedDocument.parseResult.value as CrossModelRoot; + + expect(model).toHaveProperty('entity'); + + expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); + expect(parsedDocument.parseResult.parserErrors.length).toBe(0); + + expect(model.entity?.attributes.length).toBe(6); + expect(model.entity?.attributes[0].name).toBe('Id'); + expect(model.entity?.attributes[0].name_val).toBe('Id'); + expect(model.entity?.attributes[0].datatype).toBe('int'); + }); + + test('entity with attributes coming before the description and name', async () => { + const document = entity4; + const parsedDocument = await parseDocument(services, document); + const model = parsedDocument.parseResult.value as CrossModelRoot; + + expect(model).toHaveProperty('entity'); + expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); + expect(parsedDocument.parseResult.parserErrors.length).toBe(0); + + expect(model.entity?.name).toBe('Customer'); + expect(model.entity?.name_val).toBe('Customer'); + expect(model.entity?.description).toBe('A customer with whom a transaction has been made.'); + + expect(model.entity?.attributes.length).toBe(6); + expect(model.entity?.attributes[0].name).toBe('Id'); + expect(model.entity?.attributes[0].name_val).toBe('Id'); + expect(model.entity?.attributes[0].datatype).toBe('int'); + }); + + test('entity with indentation error', async () => { + const document = entity3; + const parsedDocument = await parseDocument(services, document); + const model = parsedDocument.parseResult.value as CrossModelRoot; + + expect(model).toHaveProperty('entity'); + expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); + expect(parsedDocument.parseResult.parserErrors.length).toBe(1); + }); + }); +}); diff --git a/extensions/crossmodel-lang/test/language-server/cross-model-lang-relationship.test.ts b/extensions/crossmodel-lang/test/language-server/cross-model-lang-relationship.test.ts new file mode 100644 index 00000000..54c8838a --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/cross-model-lang-relationship.test.ts @@ -0,0 +1,46 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ + +import { describe, expect, test } from '@jest/globals'; +import { EmptyFileSystem, isReference } from 'langium'; + +import { parseDocument } from './utils/utils'; +import { relationship1, relationship2 } from './utils/test-documents/relationship/index'; + +import { CrossModelRoot } from '../../src/language-server/generated/ast'; +import { createCrossModelServices } from '../../src/language-server/cross-model-module'; + +const services = createCrossModelServices({ ...EmptyFileSystem }).CrossModel; + +describe('CrossModel language Relationship', () => { + test('Simple file for relationship', async () => { + const document = relationship1; + const parsedDocument = await parseDocument(services, document); + const model = parsedDocument.parseResult.value as CrossModelRoot; + + expect(model).toHaveProperty('relationship'); + expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); + expect(parsedDocument.parseResult.parserErrors.length).toBe(0); + + expect(model.relationship?.name).toBe('Order_Customer'); + expect(model.relationship?.name_val).toBe('Customer Order relationship'); + expect(model.relationship?.type).toBe('1:1'); + expect(model.relationship?.description).toBe('A relationship between a customer and an order.'); + + expect(isReference(model.relationship?.parent)).toBe(true); + expect(isReference(model.relationship?.child)).toBe(true); + expect(model.relationship?.parent?.$refText).toBe('Customer'); + expect(model.relationship?.child?.$refText).toBe('Order'); + }); + + test('relationship with indentation error', async () => { + const document = relationship2; + const parsedDocument = await parseDocument(services, document); + const model = parsedDocument.parseResult.value as CrossModelRoot; + + expect(model).toHaveProperty('relationship'); + expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); + expect(parsedDocument.parseResult.parserErrors.length).toBe(1); + }); +}); diff --git a/extensions/crossmodel-lang/test/language-server/lexer/cross-model-indent-stack.test.ts b/extensions/crossmodel-lang/test/language-server/lexer/cross-model-indent-stack.test.ts new file mode 100644 index 00000000..0a2d1fe6 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/lexer/cross-model-indent-stack.test.ts @@ -0,0 +1,129 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ + +import { describe, expect, test, beforeEach } from '@jest/globals'; +import { indentStack } from '../../../src/language-server/lexer/cross-model-indent-stack'; + +describe('IndentStack', () => { + beforeEach(() => { + indentStack.reset(); + }); + + describe('get', () => { + test('should return a copy of the current stack', () => { + indentStack.push(2); + indentStack.push(4); + + const stack = indentStack.get(); + + expect(stack).toEqual([0, 2, 4]); + }); + + test('should return an [0] if the stack is empty', () => { + const stack = indentStack.get(); + + expect(stack).toEqual([0]); + }); + + test('should not modify the original stack when modifying the returned array', () => { + indentStack.push(2); + indentStack.push(4); + + const stack = indentStack.get(); + stack.pop(); // Modify the returned array + + expect(stack).toEqual([0, 2]); // The returned array is modified + expect(indentStack.get()).toEqual([0, 2, 4]); // The original stack remains unchanged + }); + }); + + describe('push', () => { + test('should push the given value onto the stack', () => { + indentStack.push(2); + expect(indentStack.get()).toEqual([0, 2]); + }); + + test('should push multiple values onto the stack', () => { + indentStack.push(2); + indentStack.push(4); + indentStack.push(6); + expect(indentStack.get()).toEqual([0, 2, 4, 6]); + }); + }); + + describe('reset', () => { + test('should reset the stack to contain only the initial indentation level', () => { + indentStack.push(2); + indentStack.push(4); + indentStack.reset(); + expect(indentStack.get()).toEqual([0]); + }); + }); + + describe('pop', () => { + test('should remove and return the topmost indentation level from the stack', () => { + indentStack.push(2); + + const poppedValue = indentStack.pop(); + + expect(poppedValue).toBe(2); + expect(indentStack.get()).toEqual([0]); + }); + + test('should return undefined if the stack is empty', () => { + // Pop 0 after reset + const poppedValue1 = indentStack.pop(); + const poppedValue2 = indentStack.pop(); + + expect(poppedValue1).toBe(0); + expect(poppedValue2).toBeUndefined(); + }); + }); + + describe('length', () => { + test('should return 1 when only the initial indentation level is present', () => { + expect(indentStack.length()).toBe(1); + }); + + test('should return the length of indentation levels in the stack', () => { + indentStack.push(2); + indentStack.push(4); + expect(indentStack.length()).toBe(3); + }); + }); + + describe('getLast', () => { + test('should return the last indentation level from the stack', () => { + indentStack.push(2); + indentStack.push(4); + + const lastValue = indentStack.getLast(); + + expect(lastValue).toBe(4); + }); + + test('should throw an IndentStackError if the stack is empty', () => { + indentStack.pop(); + + expect(() => indentStack.getLast()).toThrow(); + }); + }); + + describe('findLastIndex', () => { + test('should return the index of the last occurrence of the given value in the stack', () => { + indentStack.push(2); + indentStack.push(4); + indentStack.push(2); + + const lastIndex = indentStack.findLastIndex(2); + + expect(lastIndex).toBe(3); + }); + + test('should return -1 if the given value is not found in the stack', () => { + const lastIndex = indentStack.findLastIndex(2); + expect(lastIndex).toBe(-1); + }); + }); +}); diff --git a/extensions/crossmodel-lang/test/language-server/lexer/cross-model-indentation-tokens.test.ts b/extensions/crossmodel-lang/test/language-server/lexer/cross-model-indentation-tokens.test.ts new file mode 100644 index 00000000..d431b4b1 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/lexer/cross-model-indentation-tokens.test.ts @@ -0,0 +1,306 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +import { describe, expect, test, beforeEach, beforeAll } from '@jest/globals'; +import { Lexer, TokenType, createToken, tokenMatcher } from 'chevrotain'; + +import { SPACES, NEWLINE, INDENT, DEDENT } from '../../../src/language-server/lexer/cross-model-indentation-tokens'; +import { indentStack } from '../../../src/language-server/lexer/cross-model-indent-stack'; + +describe('matchIndentBase', () => { + let TESTTOKEN: TokenType; + let LINETOKEN: TokenType; + let testLexer: Lexer; + + beforeAll(() => { + TESTTOKEN = createToken({ + name: 'TESTTOKEN', + pattern: /TESTTOKEN/ + }); + + LINETOKEN = createToken({ + name: 'LINETOKEN', + pattern: /-/ + }); + + testLexer = new Lexer([NEWLINE, DEDENT, INDENT, LINETOKEN, TESTTOKEN, SPACES]); + }); + + beforeEach(() => { + indentStack.reset(); + }); + + describe('SPACES token', () => { + test('should not produce a token for spaces between words', () => { + const input = 'TESTTOKEN TESTTOKEN'; + + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(2); + lexResult.tokens.map(token => { + expect(tokenMatcher(token, TESTTOKEN)).toBe(true); + }); + }); + + test('should not produce a token for spaces at the end of a line', () => { + const input = 'TESTTOKEN '; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(1); + lexResult.tokens.map(token => { + expect(tokenMatcher(token, TESTTOKEN)).toBe(true); + }); + }); + }); + + describe('NEWLINE token', () => { + test('should match a newline character', () => { + const input = '\n'; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(0); + expect(lexResult.groups).toHaveProperty('nl'); + expect(lexResult.groups.nl).toHaveLength(1); + expect(tokenMatcher(lexResult.groups.nl[0], NEWLINE)).toBe(true); + }); + + test('should match a newline character with carriage return', () => { + const input = '\r\n'; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(0); + expect(lexResult.groups).toHaveProperty('nl'); + expect(lexResult.groups.nl).toHaveLength(1); + expect(tokenMatcher(lexResult.groups.nl[0], NEWLINE)).toBe(true); + }); + + test('should match a newline in the middle of a line', () => { + const input = 'TESTTOKEN\nTESTTOKEN'; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(2); + expect(tokenMatcher(lexResult.tokens[0], TESTTOKEN)).toBe(true); + expect(tokenMatcher(lexResult.tokens[1], TESTTOKEN)).toBe(true); + + expect(lexResult.groups).toHaveProperty('nl'); + expect(lexResult.groups.nl).toHaveLength(1); + expect(tokenMatcher(lexResult.groups.nl[0], NEWLINE)).toBe(true); + }); + + test('should match a newline preceded by spaces', () => { + const input = ' \n'; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.groups).toHaveProperty('nl'); + expect(lexResult.groups.nl).toHaveLength(1); + expect(tokenMatcher(lexResult.groups.nl[0], NEWLINE)).toBe(true); + expect(lexResult.groups.nl[0].image).toBe('\n'); + }); + + test('should match a newline followed by spaces', () => { + const input = '\n '; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.groups).toHaveProperty('nl'); + expect(lexResult.groups.nl).toHaveLength(1); + expect(tokenMatcher(lexResult.groups.nl[0], NEWLINE)).toBe(true); + expect(lexResult.groups.nl[0].image).toBe('\n'); + }); + }); + + describe('INDENT token', () => { + test('should match indentation at the start of a line', () => { + const input = ' '; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(1); + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + expect(lexResult.tokens[0].image).toBe(input); + expect(indentStack.getLast()).toBe(4); + }); + + test('should match indentation at the start of a line, when there are other token after it', () => { + const input = ' TESTTOKEN'; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(2); + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + expect(tokenMatcher(lexResult.tokens[1], TESTTOKEN)).toBe(true); + expect(lexResult.tokens[0].image).toBe(' '); + // Check what the current indentation is + expect(indentStack.getLast()).toBe(4); + }); + + test('should match indentation at the start of a line', () => { + const input = 'TESTTOKEN\n '; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(2); + expect(tokenMatcher(lexResult.tokens[0], TESTTOKEN)).toBe(true); + expect(tokenMatcher(lexResult.tokens[1], INDENT)).toBe(true); + expect(lexResult.tokens[1].image).toBe(' '); + // Check what the current indentation is + expect(indentStack.getLast()).toBe(4); + }); + + test('should match indentation when only new lines preceding', () => { + const input = '\n\n\n\n '; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(1); + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + expect(lexResult.tokens[0].image).toBe(' '); + expect(indentStack.getLast()).toBe(4); + expect(lexResult.groups.nl).toHaveLength(4); + }); + + test('Should only match follow up indentation', () => { + const input = ' \n '; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(2); + expect(lexResult.groups.nl).toHaveLength(1); + + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + expect(lexResult.tokens[0].image).toBe(' '); + expect(tokenMatcher(lexResult.tokens[1], INDENT)).toBe(true); + expect(lexResult.tokens[1].image).toBe(' '); + expect(indentStack.getLast()).toBe(6); + }); + + // Should not match + test('should not match indentation after another token', () => { + const input = 'TESTTOKEN '; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(1); + expect(tokenMatcher(lexResult.tokens[0], TESTTOKEN)).toBe(true); + expect(indentStack.getLast()).toBe(0); + }); + + test('Should not match sucessive same indentation level', () => { + const input = ' \n '; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(1); + expect(lexResult.groups.nl).toHaveLength(1); + + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + expect(lexResult.tokens[0].image).toBe(' '); + expect(indentStack.getLast()).toBe(4); + }); + + test('Should not match lower indentation', () => { + const input = ' \n \n '; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(3); + expect(lexResult.groups.nl).toHaveLength(2); + + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + expect(lexResult.tokens[0].image).toBe(' '); + expect(tokenMatcher(lexResult.tokens[1], INDENT)).toBe(true); + expect(lexResult.tokens[1].image).toBe(' '); + expect(tokenMatcher(lexResult.tokens[2], DEDENT)).toBe(true); + expect(indentStack.getLast()).toBe(2); + }); + }); + + describe('INDENT token and lists(-)', () => { + test('should match a single level of indentation at the start of a line with -', () => { + const input = ' - '; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(2); + expect(lexResult.tokens[0].image).toBe(' '); + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + expect(tokenMatcher(lexResult.tokens[1], LINETOKEN)).toBe(true); + expect(indentStack.getLast()).toBe(6); + }); + + test('should match a indentation after indentation with -', () => { + const input = ' - \n '; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(3); + expect(lexResult.tokens[0].image).toBe(' '); + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + + expect(tokenMatcher(lexResult.tokens[1], LINETOKEN)).toBe(true); + + expect(lexResult.tokens[2].image).toBe(' '); + expect(tokenMatcher(lexResult.tokens[2], INDENT)).toBe(true); + expect(indentStack.getLast()).toBe(8); + }); + + test('should match a dedentation after indentation with -', () => { + const input = ' \n - \n '; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(4); + expect(lexResult.tokens[0].image).toBe(' '); + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + + expect(tokenMatcher(lexResult.tokens[1], INDENT)).toBe(true); + expect(lexResult.tokens[1].image).toBe(' '); + + expect(tokenMatcher(lexResult.tokens[2], LINETOKEN)).toBe(true); + + expect(tokenMatcher(lexResult.tokens[3], DEDENT)).toBe(true); + expect(indentStack.getLast()).toBe(2); + }); + + test('should not match second indentation with same level', () => { + const input = ' - \n '; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(2); + expect(lexResult.tokens[0].image).toBe(' '); + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + expect(tokenMatcher(lexResult.tokens[1], LINETOKEN)).toBe(true); + expect(indentStack.getLast()).toBe(6); + }); + }); + + describe('DEDENT token', () => { + test('should match a dedentation', () => { + const input = ' \n \n '; + + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(3); + expect(lexResult.tokens[0].image).toBe(' '); + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + + expect(tokenMatcher(lexResult.tokens[1], INDENT)).toBe(true); + expect(lexResult.tokens[1].image).toBe(' '); + + expect(tokenMatcher(lexResult.tokens[2], DEDENT)).toBe(true); + expect(indentStack.getLast()).toBe(2); + }); + + test('should match a dedentation after dedentation', () => { + const input = ' \n \n \n \n '; + + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(5); + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + expect(tokenMatcher(lexResult.tokens[1], INDENT)).toBe(true); + expect(tokenMatcher(lexResult.tokens[2], INDENT)).toBe(true); + + expect(tokenMatcher(lexResult.tokens[3], DEDENT)).toBe(true); + expect(tokenMatcher(lexResult.tokens[4], DEDENT)).toBe(true); + expect(indentStack.getLast()).toBe(2); + }); + + test('should not match a dedentation whens on the same level', () => { + const input = ' \n '; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(1); + expect(lexResult.tokens[0].image).toBe(' '); + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + }); + }); +}); diff --git a/extensions/crossmodel-lang/test/language-server/lexer/cross-model-lexer.test.ts b/extensions/crossmodel-lang/test/language-server/lexer/cross-model-lexer.test.ts new file mode 100644 index 00000000..46338d94 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/lexer/cross-model-lexer.test.ts @@ -0,0 +1,95 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ + +import { describe, expect, test, beforeAll } from '@jest/globals'; +import { EmptyFileSystem } from 'langium'; +import { tokenMatcher } from 'chevrotain'; + +import { CrossModelLexer } from '../../../src/language-server/lexer/cross-model-lexer'; +import { DEDENT, INDENT } from '../../../src/language-server/lexer/cross-model-indentation-tokens'; +import { createCrossModelServices } from '../../../src/language-server/cross-model-module'; + +const services = createCrossModelServices({ ...EmptyFileSystem }).CrossModel; + +describe('CrossModelLexer', () => { + let crossModelLexer: CrossModelLexer; + + beforeAll(() => { + crossModelLexer = new CrossModelLexer(services); + }); + + describe('Simple keywords', () => { + test('should tokenize a simple word', () => { + const input = 'entity'; + + const lexResult = crossModelLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(1); + lexResult.tokens.map(token => { + expect(token.image).toBe('entity'); + }); + }); + + test('should tokenize a couple of simple words', () => { + const input = 'entity entity entity'; + + const lexResult = crossModelLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(3); + lexResult.tokens.map(token => { + expect(token.image).toBe('entity'); + }); + }); + }); + + describe('Indentation', () => { + test('Simple indentation, should give indent and dedent token', () => { + const input = ' '; + + const lexResult = crossModelLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(2); + + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + expect(tokenMatcher(lexResult.tokens[1], DEDENT)).toBe(true); + }); + + test('single indentation but stay on same level, should give 1 indent and 1 dedent token', () => { + const input = ' \n '; + + const lexResult = crossModelLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(2); + + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + expect(tokenMatcher(lexResult.tokens[1], DEDENT)).toBe(true); + }); + + test('double indentation, should give indent and dedent token', () => { + const input = ' \n '; + + const lexResult = crossModelLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(4); + + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + expect(tokenMatcher(lexResult.tokens[1], INDENT)).toBe(true); + expect(tokenMatcher(lexResult.tokens[2], DEDENT)).toBe(true); + expect(tokenMatcher(lexResult.tokens[3], DEDENT)).toBe(true); + }); + + test('double indentation, but dedent in text', () => { + const input = ' \n \n '; + + const lexResult = crossModelLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(4); + + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + expect(tokenMatcher(lexResult.tokens[1], INDENT)).toBe(true); + expect(tokenMatcher(lexResult.tokens[2], DEDENT)).toBe(true); + expect(tokenMatcher(lexResult.tokens[3], DEDENT)).toBe(true); + }); + }); +}); diff --git a/extensions/crossmodel-lang/test/language-server/lexer/cross-model-token-generator.test.ts b/extensions/crossmodel-lang/test/language-server/lexer/cross-model-token-generator.test.ts new file mode 100644 index 00000000..77ccdea9 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/lexer/cross-model-token-generator.test.ts @@ -0,0 +1,58 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ + +import { describe, expect, test, beforeAll } from '@jest/globals'; + +import { Grammar } from 'langium'; +import { ExampleGrammarWithIndent } from '../utils/example-grammar'; +import { TokenType } from 'chevrotain'; + +import { ExampleGrammarWithNoIndent } from '../utils/example-grammar-no-indent'; +import { DEDENT, INDENT, NEWLINE, SPACES } from '../../../src/language-server/lexer/cross-model-indentation-tokens'; +import { CrossModelTokenBuilder } from '../../../src/language-server/lexer/cross-model-token-generator'; + +describe('CrossModelTokenBuilder', () => { + let tokenBuilder: CrossModelTokenBuilder; + let exampleGrammerWithIndentation: Grammar; + let exampleGrammerWithNoIndentation: Grammar; + + beforeAll(() => { + tokenBuilder = new CrossModelTokenBuilder(); + exampleGrammerWithIndentation = ExampleGrammarWithIndent(); + exampleGrammerWithNoIndentation = ExampleGrammarWithNoIndent(); + }); + + describe('buildTokens', () => { + test('Should give NEWLINE token in first spot', () => { + const tokens = tokenBuilder.buildTokens(exampleGrammerWithIndentation) as TokenType[]; + + expect(tokens[0]).toBe(NEWLINE); + }); + + test('Should give DEDENT token in second spot', () => { + const tokens = tokenBuilder.buildTokens(exampleGrammerWithIndentation) as TokenType[]; + + expect(tokens[1]).toBe(DEDENT); + }); + + test('Should give INDENT token in third spot', () => { + const tokens = tokenBuilder.buildTokens(exampleGrammerWithIndentation) as TokenType[]; + + expect(tokens[2]).toBe(INDENT); + }); + + test('Should give SPACE token in last spot', () => { + const tokens = tokenBuilder.buildTokens(exampleGrammerWithIndentation) as TokenType[]; + + const spaceToken = tokens.pop(); + expect(spaceToken).toBe(SPACES); + }); + + test('Should throw error when missing indentation in grammar', () => { + expect(() => { + tokenBuilder.buildTokens(exampleGrammerWithNoIndentation) as TokenType[]; + }).toThrow(); + }); + }); +}); diff --git a/extensions/crossmodel-lang/test/language-server/utils/example-grammar-no-indent.ts b/extensions/crossmodel-lang/test/language-server/utils/example-grammar-no-indent.ts new file mode 100644 index 00000000..ff372c56 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/example-grammar-no-indent.ts @@ -0,0 +1,1343 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ + +import { loadGrammarFromJson, Grammar } from 'langium'; + +let loadedExampleGrammarWithIndent: Grammar | undefined; +export const ExampleGrammarWithNoIndent = (): Grammar => + loadedExampleGrammarWithIndent ?? + (loadedExampleGrammarWithIndent = loadGrammarFromJson(`{ + "$type": "Grammar", + "isDeclared": true, + "name": "CrossModel", + "rules": [ + { + "$type": "ParserRule", + "name": "CrossModelRoot", + "entry": true, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Assignment", + "feature": "entity", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@1" + }, + "arguments": [] + } + }, + { + "$type": "Assignment", + "feature": "relationship", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@6" + }, + "arguments": [] + } + }, + { + "$type": "Assignment", + "feature": "diagram", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@9" + }, + "arguments": [] + } + } + ], + "cardinality": "?" + }, + "definesHiddenTokens": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "Entity", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "entity" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@2" + }, + "arguments": [], + "cardinality": "*" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + ], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "EntityFields", + "inferredType": { + "$type": "InferredType", + "name": "Entity" + }, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "name" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name_val", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "description" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "description", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "attributes" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@3" + }, + "arguments": [] + } + ] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "EntityAttributes", + "inferredType": { + "$type": "InferredType", + "name": "EntityFields" + }, + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] + }, + { + "$type": "Assignment", + "feature": "attributes", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@4" + }, + "arguments": [] + }, + "cardinality": "*" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "EntityAttribute", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "-" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@5" + }, + "arguments": [], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "EntityAttributeFields", + "inferredType": { + "$type": "InferredType", + "name": "EntityAttribute" + }, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "name" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name_val", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "datatype" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "datatype", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "description" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "description", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "Relationship", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "relationship" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@7" + }, + "arguments": [], + "cardinality": "*" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + ], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "RelationshipFields", + "inferredType": { + "$type": "InferredType", + "name": "Relationship" + }, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "name" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name_val", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "description" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "description", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "parent" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "parent", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@1" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@17" + }, + "arguments": [] + }, + "deprecatedSyntax": false + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "child" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "child", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@1" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@17" + }, + "arguments": [] + }, + "deprecatedSyntax": false + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "type" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "type", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@8" + }, + "arguments": [] + } + } + ] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "RelationshipType", + "dataType": "string", + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Keyword", + "value": "1:1" + }, + { + "$type": "Keyword", + "value": "1:n" + }, + { + "$type": "Keyword", + "value": "n:1" + }, + { + "$type": "Keyword", + "value": "n:m" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "SystemDiagram", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "diagram" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@10" + }, + "arguments": [], + "cardinality": "*" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + ], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "SystemDiagramFields", + "inferredType": { + "$type": "InferredType", + "name": "SystemDiagram" + }, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "nodes" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@11" + }, + "arguments": [] + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "edges" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@14" + }, + "arguments": [] + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "SystemDiagramNodes", + "inferredType": { + "$type": "InferredType", + "name": "SystemDiagramFields" + }, + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] + }, + { + "$type": "Assignment", + "feature": "nodes", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@12" + }, + "arguments": [] + }, + "cardinality": "*" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "DiagramNode", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "-" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@13" + }, + "arguments": [], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "DiagramNodeFields", + "inferredType": { + "$type": "InferredType", + "name": "DiagramNode" + }, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "for" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "for", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@1" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@17" + }, + "arguments": [] + }, + "deprecatedSyntax": false + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "x" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "x", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@19" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "y" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "y", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@19" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "width" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "width", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@19" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "height" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "height", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@19" + }, + "arguments": [] + } + } + ] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "SystemDiagramEdge", + "inferredType": { + "$type": "InferredType", + "name": "SystemDiagramFields" + }, + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] + }, + { + "$type": "Assignment", + "feature": "edges", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@15" + }, + "arguments": [] + }, + "cardinality": "*" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "DiagramEdge", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "-" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@16" + }, + "arguments": [], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "DiagramEdgeFields", + "inferredType": { + "$type": "InferredType", + "name": "DiagramEdge" + }, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "for" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "for", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@6" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@17" + }, + "arguments": [] + }, + "deprecatedSyntax": false + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "QualifiedName", + "dataType": "string", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "." + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + ], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "TerminalRule", + "name": "STRING", + "definition": { + "$type": "RegexToken", + "regex": "\\"[^\\"]*\\"|'[^']*'" + }, + "fragment": false, + "hidden": false + }, + { + "$type": "TerminalRule", + "name": "NUMBER", + "type": { + "$type": "ReturnType", + "name": "number" + }, + "definition": { + "$type": "RegexToken", + "regex": "(-)?[0-9]+(\\\\.[0-9]*)?" + }, + "fragment": false, + "hidden": false + }, + { + "$type": "TerminalRule", + "hidden": true, + "name": "SL_COMMENT", + "definition": { + "$type": "RegexToken", + "regex": "#[^\\\\n\\\\r]*" + }, + "fragment": false + }, + { + "$type": "TerminalRule", + "hidden": true, + "name": "NEWLINE", + "definition": { + "$type": "CharacterRange", + "left": { + "$type": "Keyword", + "value": "this_string_does_not_matter_newline#$%^&*((" + } + }, + "fragment": false + }, + { + "$type": "TerminalRule", + "name": "DEDENT", + "definition": { + "$type": "CharacterRange", + "left": { + "$type": "Keyword", + "value": "this_string_does_not_matter_dedent#$%^&*((" + } + }, + "fragment": false, + "hidden": false + }, + { + "$type": "TerminalRule", + "hidden": true, + "name": "SPACES", + "definition": { + "$type": "CharacterRange", + "left": { + "$type": "Keyword", + "value": "this_string_does_not_matter_spaces#$%^&*((" + } + }, + "fragment": false + } + ], + "definesHiddenTokens": false, + "hiddenTokens": [], + "imports": [], + "interfaces": [], + "types": [], + "usedGrammars": [] +}`)); diff --git a/extensions/crossmodel-lang/test/language-server/utils/example-grammar.ts b/extensions/crossmodel-lang/test/language-server/utils/example-grammar.ts new file mode 100644 index 00000000..ab5f75a7 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/example-grammar.ts @@ -0,0 +1,1356 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ + +import { loadGrammarFromJson, Grammar } from 'langium'; + +let loadedExampleGrammarWithIndent: Grammar | undefined; +export const ExampleGrammarWithIndent = (): Grammar => + loadedExampleGrammarWithIndent ?? + (loadedExampleGrammarWithIndent = loadGrammarFromJson(`{ + "$type": "Grammar", + "isDeclared": true, + "name": "CrossModel", + "rules": [ + { + "$type": "ParserRule", + "name": "CrossModelRoot", + "entry": true, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Assignment", + "feature": "entity", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@1" + }, + "arguments": [] + } + }, + { + "$type": "Assignment", + "feature": "relationship", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@6" + }, + "arguments": [] + } + }, + { + "$type": "Assignment", + "feature": "diagram", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@9" + }, + "arguments": [] + } + } + ], + "cardinality": "?" + }, + "definesHiddenTokens": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "Entity", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "entity" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@2" + }, + "arguments": [], + "cardinality": "*" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + ], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "EntityFields", + "inferredType": { + "$type": "InferredType", + "name": "Entity" + }, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "name" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name_val", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "description" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "description", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "attributes" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@3" + }, + "arguments": [] + } + ] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "EntityAttributes", + "inferredType": { + "$type": "InferredType", + "name": "EntityFields" + }, + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] + }, + { + "$type": "Assignment", + "feature": "attributes", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@4" + }, + "arguments": [] + }, + "cardinality": "*" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "EntityAttribute", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "-" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@5" + }, + "arguments": [], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "EntityAttributeFields", + "inferredType": { + "$type": "InferredType", + "name": "EntityAttribute" + }, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "name" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name_val", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "datatype" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "datatype", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "description" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "description", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "Relationship", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "relationship" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@7" + }, + "arguments": [], + "cardinality": "*" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + ], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "RelationshipFields", + "inferredType": { + "$type": "InferredType", + "name": "Relationship" + }, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "name" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name_val", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "description" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "description", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "parent" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "parent", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@1" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@17" + }, + "arguments": [] + }, + "deprecatedSyntax": false + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "child" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "child", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@1" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@17" + }, + "arguments": [] + }, + "deprecatedSyntax": false + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "type" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "type", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@8" + }, + "arguments": [] + } + } + ] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "RelationshipType", + "dataType": "string", + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Keyword", + "value": "1:1" + }, + { + "$type": "Keyword", + "value": "1:n" + }, + { + "$type": "Keyword", + "value": "n:1" + }, + { + "$type": "Keyword", + "value": "n:m" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "SystemDiagram", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "diagram" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@10" + }, + "arguments": [], + "cardinality": "*" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + ], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "SystemDiagramFields", + "inferredType": { + "$type": "InferredType", + "name": "SystemDiagram" + }, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "nodes" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@11" + }, + "arguments": [] + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "edges" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@14" + }, + "arguments": [] + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "SystemDiagramNodes", + "inferredType": { + "$type": "InferredType", + "name": "SystemDiagramFields" + }, + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] + }, + { + "$type": "Assignment", + "feature": "nodes", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@12" + }, + "arguments": [] + }, + "cardinality": "*" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "DiagramNode", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "-" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@13" + }, + "arguments": [], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "DiagramNodeFields", + "inferredType": { + "$type": "InferredType", + "name": "DiagramNode" + }, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "for" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "for", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@1" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@17" + }, + "arguments": [] + }, + "deprecatedSyntax": false + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "x" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "x", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@19" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "y" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "y", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@19" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "width" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "width", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@19" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "height" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "height", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@19" + }, + "arguments": [] + } + } + ] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "SystemDiagramEdge", + "inferredType": { + "$type": "InferredType", + "name": "SystemDiagramFields" + }, + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] + }, + { + "$type": "Assignment", + "feature": "edges", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@15" + }, + "arguments": [] + }, + "cardinality": "*" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "DiagramEdge", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "-" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@16" + }, + "arguments": [], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "DiagramEdgeFields", + "inferredType": { + "$type": "InferredType", + "name": "DiagramEdge" + }, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "for" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "for", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@6" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@17" + }, + "arguments": [] + }, + "deprecatedSyntax": false + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "QualifiedName", + "dataType": "string", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "." + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + ], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "TerminalRule", + "name": "STRING", + "definition": { + "$type": "RegexToken", + "regex": "\\"[^\\"]*\\"|'[^']*'" + }, + "fragment": false, + "hidden": false + }, + { + "$type": "TerminalRule", + "name": "NUMBER", + "type": { + "$type": "ReturnType", + "name": "number" + }, + "definition": { + "$type": "RegexToken", + "regex": "(-)?[0-9]+(\\\\.[0-9]*)?" + }, + "fragment": false, + "hidden": false + }, + { + "$type": "TerminalRule", + "hidden": true, + "name": "SL_COMMENT", + "definition": { + "$type": "RegexToken", + "regex": "#[^\\\\n\\\\r]*" + }, + "fragment": false + }, + { + "$type": "TerminalRule", + "hidden": true, + "name": "NEWLINE", + "definition": { + "$type": "CharacterRange", + "left": { + "$type": "Keyword", + "value": "this_string_does_not_matter_newline#$%^&*((" + } + }, + "fragment": false + }, + { + "$type": "TerminalRule", + "name": "DEDENT", + "definition": { + "$type": "CharacterRange", + "left": { + "$type": "Keyword", + "value": "this_string_does_not_matter_dedent#$%^&*((" + } + }, + "fragment": false, + "hidden": false + }, + { + "$type": "TerminalRule", + "name": "INDENT", + "definition": { + "$type": "CharacterRange", + "left": { + "$type": "Keyword", + "value": "this_string_does_not_matter_indent#$%^&*((" + } + }, + "fragment": false, + "hidden": false + }, + { + "$type": "TerminalRule", + "hidden": true, + "name": "SPACES", + "definition": { + "$type": "CharacterRange", + "left": { + "$type": "Keyword", + "value": "this_string_does_not_matter_spaces#$%^&*((" + } + }, + "fragment": false + } + ], + "definesHiddenTokens": false, + "hiddenTokens": [], + "imports": [], + "interfaces": [], + "types": [], + "usedGrammars": [] +}`)); diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram1.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram1.ts new file mode 100644 index 00000000..493fbe17 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram1.ts @@ -0,0 +1,5 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export const diagram1 = `diagram: + id: "Systemdiagram1"`; diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram2.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram2.ts new file mode 100644 index 00000000..8c6b41f9 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram2.ts @@ -0,0 +1,12 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export const diagram2 = `diagram: + id: "Systemdiagram1" + nodes: + - id: 'CustomerNode' + for: 'Customer' + x: 100 + y: 100 + height: 100 + width: 100`; diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram3.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram3.ts new file mode 100644 index 00000000..8e204013 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram3.ts @@ -0,0 +1,8 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export const diagram3 = `diagram: + id: "Systemdiagram1" + edges: + - id: 'OrderCustomerEdge' + for: 'Order_Customer'`; diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram4.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram4.ts new file mode 100644 index 00000000..bb4fbdea --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram4.ts @@ -0,0 +1,5 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export const diagram4 = `diagram: +id: "Systemdiagram1"`; diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram5.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram5.ts new file mode 100644 index 00000000..4155ae39 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram5.ts @@ -0,0 +1,17 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export const diagram5 = `diagram: + id: "Systemdiagram1" + name: "System diagram 1" + description: "This is a basic diagram with nodes and edges" + edges: + - id: 'OrderCustomerEdge' + for: 'Order_Customer' + nodes: + - id: 'CustomerNode' + for: 'Customer' + x: 100 + y: 100 + height: 100 + width: 100`; diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram6.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram6.ts new file mode 100644 index 00000000..d8cab3b3 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram6.ts @@ -0,0 +1,17 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export const diagram6 = `diagram: + id: "Systemdiagram1" + edges: + - id: 'OrderCustomerEdge' + for: 'Order_Customer' + nodes: + - id: 'CustomerNode' + for: 'Customer' + x: 100 + y: 100 + height: 100 + width: 100 + name: "System diagram 1" + description: "This is a basic diagram with nodes and edges"`; diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/index.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/index.ts new file mode 100644 index 00000000..8cc7d55f --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/index.ts @@ -0,0 +1,9 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export * from './diagram1'; +export * from './diagram2'; +export * from './diagram3'; +export * from './diagram4'; +export * from './diagram5'; +export * from './diagram6'; diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity1.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity1.ts new file mode 100644 index 00000000..2673bf71 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity1.ts @@ -0,0 +1,7 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export const entity1 = `entity: + id: 'Customer' + name: 'Customer' + description: 'A customer with whom a transaction has been made.'`; diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity2.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity2.ts new file mode 100644 index 00000000..4c471bc1 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity2.ts @@ -0,0 +1,26 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export const entity2 = `entity: + id: 'Customer' + name: 'Customer' + description: 'A customer with whom a transaction has been made.' + attributes: + - id: 'Id' + name: 'Id' + datatype: 'int' + - id: 'FirstName' + name: 'FirstName' + datatype: 'varchar' + - id: 'LastName' + name: 'LastName' + datatype: 'varchar' + - id: 'City' + name: 'City' + datatype: 'varchar' + - id: 'Country' + name: 'Country' + datatype: 'varchar' + - id: 'Phone' + name: 'Phone' + datatype: 'varchar'`; diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity3.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity3.ts new file mode 100644 index 00000000..7dd97165 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity3.ts @@ -0,0 +1,26 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export const entity3 = `entity: + id: 'Customer' + name: 'Customer' + description: 'A customer with whom a transaction has been made.' +attributes: + - id: 'Id' + name: 'Id' + datatype: 'int' + - id: 'FirstName' + name: 'FirstName' + datatype: 'varchar' + - id: 'LastName' + name: 'LastName' + datatype: 'varchar' + - id: 'City' + name: 'City' + datatype: 'varchar' + - id: 'Country' + name: 'Country' + datatype: 'varchar' + - id: 'Phone' + name: 'Phone' + datatype: 'varchar'`; diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity4.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity4.ts new file mode 100644 index 00000000..30a2b35a --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity4.ts @@ -0,0 +1,26 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export const entity4 = `entity: + id: 'Customer' + attributes: + - id: 'Id' + name: 'Id' + datatype: 'int' + - id: 'FirstName' + name: 'FirstName' + datatype: 'varchar' + - id: 'LastName' + name: 'LastName' + datatype: 'varchar' + - id: 'City' + name: 'City' + datatype: 'varchar' + - id: 'Country' + name: 'Country' + datatype: 'varchar' + - id: 'Phone' + name: 'Phone' + datatype: 'varchar' + name: 'Customer' + description: 'A customer with whom a transaction has been made.'`; diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/index.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/index.ts new file mode 100644 index 00000000..c96f8df6 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/index.ts @@ -0,0 +1,7 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export * from './entity1'; +export * from './entity2'; +export * from './entity3'; +export * from './entity4'; diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/relationship/index.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/relationship/index.ts new file mode 100644 index 00000000..9eee07d4 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/relationship/index.ts @@ -0,0 +1,5 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export * from './relationship1'; +export * from './relationship2'; diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/relationship/relationship1.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/relationship/relationship1.ts new file mode 100644 index 00000000..569bfb9d --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/relationship/relationship1.ts @@ -0,0 +1,10 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export const relationship1 = `relationship: + id: 'Order_Customer' + parent: 'Customer' + child: 'Order' + type: 1:1 + name: "Customer Order relationship" + description: "A relationship between a customer and an order."`; diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/relationship/relationship2.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/relationship/relationship2.ts new file mode 100644 index 00000000..df39b1c0 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/relationship/relationship2.ts @@ -0,0 +1,8 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export const relationship2 = `relationship: + id: 'Order_Customer' +parent: 'Customer' + child: 'Order' + type: 1:1`; diff --git a/extensions/crossmodel-lang/test/language-server/utils/utils.ts b/extensions/crossmodel-lang/test/language-server/utils/utils.ts new file mode 100644 index 00000000..bcf2ea34 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/utils.ts @@ -0,0 +1,26 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +import { AstNode, LangiumDocument, LangiumServices } from 'langium'; +import { URI } from 'vscode-uri'; + +export async function parseDocument(services: LangiumServices, input: string): Promise> { + const document = await parseHelper(services)(input); + if (!document.parseResult) { + throw new Error('Could not parse document'); + } + return document; +} + +export function parseHelper(services: LangiumServices): (input: string) => Promise> { + const metaData = services.LanguageMetaData; + const documentBuilder = services.shared.workspace.DocumentBuilder; + return async input => { + const randomNumber = Math.floor(Math.random() * 10000000) + 1000000; + const uri = URI.parse(`file:///${randomNumber}${metaData.fileExtensions[0]}`); + const document = services.shared.workspace.LangiumDocumentFactory.fromString(input, uri); + services.shared.workspace.LangiumDocuments.addDocument(document); + await documentBuilder.build([document]); + return document; + }; +} diff --git a/package.json b/package.json index 9ac73d1e..172fa706 100644 --- a/package.json +++ b/package.json @@ -8,17 +8,17 @@ "extensions/*" ], "scripts": { + "clean": "lerna run clean && rimraf node_modules", "postinstall": "theia check:theia-version", "lint": "lerna run lint", "prepare": "lerna run prepare", "start:browser": "yarn theia:browser start", "start:electron": "yarn theia:electron start", "start:verdaccio": "yarn verdaccio --config verdaccio-config.yaml", - "test": "vitest --config configs/vitest.config.ts", + "test": "lerna run test && vitest --config configs/vitest.config.ts", "theia:browser": "yarn --cwd applications/browser-app", "theia:electron": "yarn --cwd applications/electron-app", - "watch": "lerna run --parallel watch", - "clean": "lerna run clean && rimraf node_modules" + "watch": "lerna run --parallel watch" }, "devDependencies": { "@testing-library/react": "^11.2.7", diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index 02b3dcd1..f90a8416 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -4,5 +4,5 @@ "noEmit": true }, "exclude": ["**/node_modules", "**/.eslintrc.js"], - "include": ["packages/*/src", "applications/*/src", "applications/*/scripts", "extensions/*/src"] + "include": ["packages/*/src", "applications/*/src", "applications/*/scripts", "extensions/*/src", "extensions/*/test"] } From ac5ea1cb46fe179bc9e99e8ac6fa59602dee0fbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96zcan=20Seker?= Date: Thu, 17 Aug 2023 14:02:42 +0200 Subject: [PATCH 04/22] GmodelFactory refactor, serailizer broken, fixing that first --- .../voorbeeld_taal_entity_customer.cm | 0 .../voorbeeld_taal_entity_order.cm | 0 ...oorbeeld_taal_relationship.relationship.cm | 0 .../voorbeeld_taal_diagram.diagram.cm | 0 .../model/builders/gentity-node.ts | 73 ---------------- .../model/builders/node-builder.ts | 87 +++++++++++++++++++ .../model/cross-model-gmodel-factory.ts | 70 ++++++++------- .../language-server/cross-model-validator.ts | 2 - .../browser/react-components/ModelContext.tsx | 2 +- .../tabs/EntityAttributesTab.tsx | 8 +- .../protocol/src/model-service/protocol.ts | 45 +++++----- 11 files changed, 153 insertions(+), 134 deletions(-) rename examples/yaml-example/{ => entities}/voorbeeld_taal_entity_customer.cm (100%) rename examples/yaml-example/{ => entities}/voorbeeld_taal_entity_order.cm (100%) rename examples/yaml-example/{ => relationships}/voorbeeld_taal_relationship.relationship.cm (100%) rename examples/yaml-example/{ => source}/voorbeeld_taal_diagram.diagram.cm (100%) delete mode 100644 extensions/crossmodel-lang/src/glsp-server/model/builders/gentity-node.ts create mode 100644 extensions/crossmodel-lang/src/glsp-server/model/builders/node-builder.ts diff --git a/examples/yaml-example/voorbeeld_taal_entity_customer.cm b/examples/yaml-example/entities/voorbeeld_taal_entity_customer.cm similarity index 100% rename from examples/yaml-example/voorbeeld_taal_entity_customer.cm rename to examples/yaml-example/entities/voorbeeld_taal_entity_customer.cm diff --git a/examples/yaml-example/voorbeeld_taal_entity_order.cm b/examples/yaml-example/entities/voorbeeld_taal_entity_order.cm similarity index 100% rename from examples/yaml-example/voorbeeld_taal_entity_order.cm rename to examples/yaml-example/entities/voorbeeld_taal_entity_order.cm diff --git a/examples/yaml-example/voorbeeld_taal_relationship.relationship.cm b/examples/yaml-example/relationships/voorbeeld_taal_relationship.relationship.cm similarity index 100% rename from examples/yaml-example/voorbeeld_taal_relationship.relationship.cm rename to examples/yaml-example/relationships/voorbeeld_taal_relationship.relationship.cm diff --git a/examples/yaml-example/voorbeeld_taal_diagram.diagram.cm b/examples/yaml-example/source/voorbeeld_taal_diagram.diagram.cm similarity index 100% rename from examples/yaml-example/voorbeeld_taal_diagram.diagram.cm rename to examples/yaml-example/source/voorbeeld_taal_diagram.diagram.cm diff --git a/extensions/crossmodel-lang/src/glsp-server/model/builders/gentity-node.ts b/extensions/crossmodel-lang/src/glsp-server/model/builders/gentity-node.ts deleted file mode 100644 index ae12aba6..00000000 --- a/extensions/crossmodel-lang/src/glsp-server/model/builders/gentity-node.ts +++ /dev/null @@ -1,73 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2023 CrossBreeze. - ********************************************************************************/ - -// export class GEntityNode extends GNode { -// override type = 'node:entity'; - -// static override builder(): GEntityNodeBuilder { -// return new GEntityNodeBuilder(GEntityNode).type('node:entity'); -// } -// } - -// export class GEntityNodeBuilder extends GNodeBuilder { -// addNode(node: DiagramNode): this { -// // Get the reference that the DiagramNode holds to the Entity in the .langium file. -// const entityRef = node.semanticElement.ref; - -// // Options which are the same for every node -// this.addCssClasses('diagram-node', 'entity').layout('vbox').addArgs(ArgsUtil.cornerRadius(3)); - -// // We need the id before we can build the label and childeren. -// if (this.id === undefined) { -// throw new Error('Add id to builder before adding the node reference.'); -// } - -// // Add the label/name of the node -// const label = GCompartment.builder() -// .layout('hbox') -// .addLayoutOption('hAlign', 'center') -// .addCssClass('entity-header-compartment') -// .add( -// GLabel.builder() -// .text(entityRef?.name || 'unresolved') -// .id(`${this.proxy.id}_label`) -// .addCssClass('entity-header-label') -// .build() -// ) -// .build(); - -// this.add(label); - -// // Add the children of the node -// if (entityRef !== undefined) { -// const allAttributesCompartment = GCompartment.builder() -// .addCssClass('attributes-compartment') -// .layout('vbox') -// .addLayoutOption('hAlign', 'left') -// .addLayoutOption('paddingBottom', 0); - -// // Add the attributes of the entity. -// for (const attribute of entityRef.attributes) { -// const attributeCompartment = GCompartment.builder() -// .addCssClass('attribute-compartment') -// .layout('hbox') -// .addLayoutOption('paddingBottom', 3) -// .addLayoutOption('paddingTop', 3); - -// attributeCompartment.add(GLabel.builder().text(attribute.name).addCssClass('attribute').build()); -// attributeCompartment.add(GLabel.builder().text(' : ').build()); -// attributeCompartment.add(GLabel.builder().text(attribute.value.toString()).addCssClass('datatype').build()); - -// allAttributesCompartment.add(attributeCompartment.build()); -// } - -// this.add(allAttributesCompartment.build()); -// } - -// // The DiagramNode in the langium file holds the coordinates of node -// this.addLayoutOption('prefWidth', node.width).addLayoutOption('prefHeight', node.height).position(node.x, node.y); - -// return this; -// } -// } diff --git a/extensions/crossmodel-lang/src/glsp-server/model/builders/node-builder.ts b/extensions/crossmodel-lang/src/glsp-server/model/builders/node-builder.ts new file mode 100644 index 00000000..f5a23035 --- /dev/null +++ b/extensions/crossmodel-lang/src/glsp-server/model/builders/node-builder.ts @@ -0,0 +1,87 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +import { ArgsUtil, GCompartment, GLabel, GNode, GNodeBuilder } from '@eclipse-glsp/server'; +import { DiagramNode } from '../../../language-server/generated/ast'; + +export class GEntityNode extends GNode { + override type = 'node:entity'; + + static override builder(): GEntityNodeBuilder { + return new GEntityNodeBuilder(GEntityNode).type('node:entity'); + } +} + +export class GEntityNodeBuilder extends GNodeBuilder { + addNode(node: DiagramNode): this { + // Get the reference that the DiagramNode holds to the Entity in the .langium file. + const entityRef = node.for?.ref; + + // Options which are the same for every node + this.addCssClasses('diagram-node', 'entity').layout('vbox').addArgs(ArgsUtil.cornerRadius(3)); + + // We need the id before we can build the label and childeren. + if (this.id === undefined) { + throw new Error('Add id to builder before adding the node reference.'); + } + + // Add the label/name of the node + const label = GCompartment.builder() + .layout('hbox') + .addLayoutOption('hAlign', 'center') + .addCssClass('entity-header-compartment') + .add( + GLabel.builder() + .text(entityRef?.name || 'unresolved') + .id(`${this.proxy.id}_label`) + .addCssClass('entity-header-label') + .build() + ) + .build(); + + this.add(label); + + // Add the children of the node + if (entityRef !== undefined) { + const allAttributesCompartment = GCompartment.builder() + .addCssClass('attributes-compartment') + .layout('vbox') + .addLayoutOption('hAlign', 'left') + .addLayoutOption('paddingBottom', 0); + + // Add the attributes of the entity. + for (const attribute of entityRef.attributes) { + const attributeCompartment = GCompartment.builder() + .addCssClass('attribute-compartment') + .layout('hbox') + .addLayoutOption('paddingBottom', 3) + .addLayoutOption('paddingTop', 3); + + attributeCompartment.add( + GLabel.builder() + .text(attribute.name_val || '') + .addCssClass('attribute') + .build() + ); + attributeCompartment.add(GLabel.builder().text(' : ').build()); + attributeCompartment.add( + GLabel.builder() + .text(attribute.datatype?.toString() || '') + .addCssClass('datatype') + .build() + ); + + allAttributesCompartment.add(attributeCompartment.build()); + } + + this.add(allAttributesCompartment.build()); + } + + // The DiagramNode in the langium file holds the coordinates of node + this.addLayoutOption('prefWidth', node.width || 100) + .addLayoutOption('prefHeight', node.height || 100) + .position(node.x || 100, node.y || 100); + + return this; + } +} diff --git a/extensions/crossmodel-lang/src/glsp-server/model/cross-model-gmodel-factory.ts b/extensions/crossmodel-lang/src/glsp-server/model/cross-model-gmodel-factory.ts index ad998ca7..86f051c8 100644 --- a/extensions/crossmodel-lang/src/glsp-server/model/cross-model-gmodel-factory.ts +++ b/extensions/crossmodel-lang/src/glsp-server/model/cross-model-gmodel-factory.ts @@ -1,11 +1,11 @@ /******************************************************************************** * Copyright (c) 2023 CrossBreeze. ********************************************************************************/ -import { GModelFactory } from '@eclipse-glsp/server'; +import { GEdge, GGraph, GLabel, GModelFactory, GNode } from '@eclipse-glsp/server'; import { inject, injectable } from 'inversify'; -// import { DiagramEdge, DiagramNode } from '../../language-server/generated/ast'; -// import { GEntityNode } from './builders/gentity-node'; +import { DiagramEdge, DiagramNode } from '../../language-server/generated/ast'; import { CrossModelState } from './cross-model-state'; +import { GEntityNode } from './builders/node-builder'; /** * Custom factory that translates the semantic diagram root from Langium to a GLSP graph. @@ -17,36 +17,40 @@ export class CrossModelGModelFactory implements GModelFactory { @inject(CrossModelState) protected readonly modelState: CrossModelState; createModel(): void { - // const newRoot = this.createGraph(); - // if (newRoot) { - // // update GLSP root element in state so it can be used in any follow-up actions/commands - // this.modelState.updateRoot(newRoot); - // } + const newRoot = this.createGraph(); + if (newRoot) { + // update GLSP root element in state so it can be used in any follow-up actions/commands + this.modelState.updateRoot(newRoot); + } } - // protected createGraph(): GGraph | undefined { - // const diagramRoot = this.modelState.diagramRoot; - - // const graphBuilder = GGraph.builder().id(this.modelState.semanticUri); - // diagramRoot.nodes.map(node => this.createDiagramNode(node)).forEach(node => graphBuilder.add(node)); - // diagramRoot.edges.map(edge => this.createDiagramEdge(edge)).forEach(edge => graphBuilder.add(edge)); - - // return graphBuilder.build(); - // } - - // protected createDiagramNode(node: DiagramNode): GNode { - // // Get the reference that the DiagramNode holds to the Entity in the .langium file. - // const id = this.modelState.index.createId(node) ?? 'unknown'; - // return GEntityNode.builder().id(id).addNode(node).build(); - // } - - // protected createDiagramEdge(edge: DiagramEdge): GEdge { - // const id = this.modelState.index.createId(edge) ?? 'unknown'; - // return GEdge.builder() - // .id(id) - // .addCssClasses('diagram-edge', 'relationship') - // .sourceId(edge.source.$refText) - // .targetId(edge.target.$refText) - // .build(); - // } + protected createGraph(): GGraph | undefined { + const diagramRoot = this.modelState.diagramRoot; + + const graphBuilder = GGraph.builder().id(this.modelState.semanticUri); + diagramRoot.nodes.map(node => this.createDiagramNode(node)).forEach(node => graphBuilder.add(node)); + // diagramRoot.edges.map(edge => this.createDiagramEdge(edge)).forEach(edge => graphBuilder.add(edge)); + + return graphBuilder.build(); + } + + protected createDiagramNode(node: DiagramNode): GNode { + // Get the reference that the DiagramNode holds to the Entity in the .langium file. + const id = this.modelState.index.createId(node) ?? 'unknown'; + + return GEntityNode.builder().id(id).addNode(node).build(); + } + + protected createDiagramEdge(edge: DiagramEdge): GEdge { + const id = this.modelState.index.createId(edge) ?? 'unknown'; + const parentRef = edge.for?.ref?.parent?.$refText; + const childRef = edge.for?.ref?.child?.$refText; + + return GEdge.builder() + .id(id) + .addCssClasses('diagram-edge', 'relationship') + .sourceId(parentRef || '') + .targetId(childRef || '') + .build(); + } } diff --git a/extensions/crossmodel-lang/src/language-server/cross-model-validator.ts b/extensions/crossmodel-lang/src/language-server/cross-model-validator.ts index 3498ae1d..54b79393 100644 --- a/extensions/crossmodel-lang/src/language-server/cross-model-validator.ts +++ b/extensions/crossmodel-lang/src/language-server/cross-model-validator.ts @@ -26,14 +26,12 @@ export function registerValidationChecks(services: CrossModelServices): void { */ export class CrossModelValidator { checkSystemDiagramHasNecessaryFields(system: SystemDiagram, accept: ValidationAcceptor): void { - console.log('test', system); if (!system.name) { accept('error', 'Systemdiagram missing id field', { node: system, property: 'name' }); } } checkEntityHasNecessaryFields(entity: Entity, accept: ValidationAcceptor): void { - console.log('test', entity); if (!entity.name) { accept('error', 'Entity missing id field', { node: entity, property: 'name' }); } diff --git a/packages/form-client/src/browser/react-components/ModelContext.tsx b/packages/form-client/src/browser/react-components/ModelContext.tsx index 5ceb0630..4fb5d884 100644 --- a/packages/form-client/src/browser/react-components/ModelContext.tsx +++ b/packages/form-client/src/browser/react-components/ModelContext.tsx @@ -87,7 +87,7 @@ export function ModelReducer(model: CrossModelRoot, action: any): CrossModelRoot throw Error('action.id or dataType is undefined'); } - model.entity.attributes[action.id].value = action.dataType; + model.entity.attributes[action.id].datatype = action.dataType; return model; diff --git a/packages/form-client/src/browser/react-components/entity-components/tabs/EntityAttributesTab.tsx b/packages/form-client/src/browser/react-components/entity-components/tabs/EntityAttributesTab.tsx index b900b37a..6408ec72 100644 --- a/packages/form-client/src/browser/react-components/entity-components/tabs/EntityAttributesTab.tsx +++ b/packages/form-client/src/browser/react-components/entity-components/tabs/EntityAttributesTab.tsx @@ -17,7 +17,7 @@ import { } from '@mui/x-data-grid'; import { Checkbox, FormControl, MenuItem, Select, SelectChangeEvent } from '@mui/material'; import { ModelContext, ModelDispatchContext, ModelReducer } from '../../ModelContext'; -import { Attribute, CrossModelRoot } from '@crossbreeze/protocol'; +import { EntityAttribute, CrossModelRoot } from '@crossbreeze/protocol'; export function EntityAttributesTab(): React.ReactElement { // Context variables to handle model state. @@ -118,19 +118,19 @@ function CheckboxCell(params: GridCellParams): React.ReactElement { return ; } -function createRows(attributes: Array): GridRowsProp { +function createRows(attributes: Array): GridRowsProp { const rows: any = []; for (const key in attributes) { if (attributes.hasOwnProperty.call(attributes, key)) { - const item: Attribute = attributes[key]; + const item: EntityAttribute = attributes[key]; rows.push({ id: parseInt(key, 10), name: item.name, key: false, required: false, - value: item.value, + datatype: item.datatype, length: undefined, scale: undefined, precision: undefined, diff --git a/packages/protocol/src/model-service/protocol.ts b/packages/protocol/src/model-service/protocol.ts index 6519ab4f..4e481eb8 100644 --- a/packages/protocol/src/model-service/protocol.ts +++ b/packages/protocol/src/model-service/protocol.ts @@ -13,33 +13,36 @@ export interface CrossModelRoot { relationship?: Relationship; } -export interface Relationship { - readonly $type: 'Relationship'; - name: string; - properties: Array; - source: string; - target: string; - type: '1:1' | '1:n' | 'n:1' | 'n:m'; +export interface Entity { + readonly $container: CrossModelRoot; + readonly $type: 'Entity'; + attributes: Array; + description?: string; + name?: string; + name_val?: string; } -export interface Property { - readonly $type: 'Property'; - key: string; - value: number | string; +export interface EntityAttribute { + readonly $container: Entity; + readonly $type: 'EntityAttribute'; + datatype?: string; + description?: string; + name?: string; + name_val?: string; } -export interface Entity { - readonly $type: 'Entity'; - name: string; - description: string; - attributes: Array; +export interface Relationship { + readonly $container: CrossModelRoot; + readonly $type: 'Relationship'; + child?: string; + description?: string; + name?: string; + name_val?: string; + parent?: string; + type?: RelationshipType; } -export interface Attribute { - readonly $type: 'Attribute'; - name: string; - value: number | string; -} +export type RelationshipType = '1:1' | '1:n' | 'n:1' | 'n:m'; export const OpenModel = new rpc.RequestType1('server/open'); export const CloseModel = new rpc.RequestType1('server/close'); From df6a8b9b59c52ca5640dde22fdbdf365440ec10c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96zcan=20Seker?= Date: Fri, 18 Aug 2023 11:24:28 +0200 Subject: [PATCH 05/22] Rewrote serializer, now dynamic and works with Yaml --- .vscode/launch.json | 16 +- .../language-server/cross-model-serializer.ts | 82 ++--- .../cross-model-lang-diagram.test.ts | 4 +- .../cross-model-lang-entity.test.ts | 4 +- .../cross-model-lang-relationship.test.ts | 4 +- .../lexer/cross-model-token-generator.test.ts | 4 +- .../serializer/cross-model-serializer.test.ts | 280 ++++++++++++++++++ .../example-grammar-no-indent.ts | 0 .../{utils => test-utils}/example-grammar.ts | 0 .../test-documents/diagram/diagram1.ts | 0 .../test-documents/diagram/diagram2.ts | 0 .../test-documents/diagram/diagram3.ts | 0 .../test-documents/diagram/diagram4.ts | 0 .../test-documents/diagram/diagram5.ts | 0 .../test-documents/diagram/diagram6.ts | 0 .../test-documents/diagram/index.ts | 0 .../test-documents/entity/entity1.ts | 0 .../test-documents/entity/entity2.ts | 0 .../test-documents/entity/entity3.ts | 0 .../test-documents/entity/entity4.ts | 0 .../test-documents/entity/index.ts | 0 .../test-documents/relationship/index.ts | 0 .../relationship/relationship1.ts | 0 .../relationship/relationship2.ts | 0 .../{utils => test-utils}/utils.ts | 0 package.json | 1 - 26 files changed, 346 insertions(+), 49 deletions(-) create mode 100644 extensions/crossmodel-lang/test/language-server/serializer/cross-model-serializer.test.ts rename extensions/crossmodel-lang/test/language-server/{utils => test-utils}/example-grammar-no-indent.ts (100%) rename extensions/crossmodel-lang/test/language-server/{utils => test-utils}/example-grammar.ts (100%) rename extensions/crossmodel-lang/test/language-server/{utils => test-utils}/test-documents/diagram/diagram1.ts (100%) rename extensions/crossmodel-lang/test/language-server/{utils => test-utils}/test-documents/diagram/diagram2.ts (100%) rename extensions/crossmodel-lang/test/language-server/{utils => test-utils}/test-documents/diagram/diagram3.ts (100%) rename extensions/crossmodel-lang/test/language-server/{utils => test-utils}/test-documents/diagram/diagram4.ts (100%) rename extensions/crossmodel-lang/test/language-server/{utils => test-utils}/test-documents/diagram/diagram5.ts (100%) rename extensions/crossmodel-lang/test/language-server/{utils => test-utils}/test-documents/diagram/diagram6.ts (100%) rename extensions/crossmodel-lang/test/language-server/{utils => test-utils}/test-documents/diagram/index.ts (100%) rename extensions/crossmodel-lang/test/language-server/{utils => test-utils}/test-documents/entity/entity1.ts (100%) rename extensions/crossmodel-lang/test/language-server/{utils => test-utils}/test-documents/entity/entity2.ts (100%) rename extensions/crossmodel-lang/test/language-server/{utils => test-utils}/test-documents/entity/entity3.ts (100%) rename extensions/crossmodel-lang/test/language-server/{utils => test-utils}/test-documents/entity/entity4.ts (100%) rename extensions/crossmodel-lang/test/language-server/{utils => test-utils}/test-documents/entity/index.ts (100%) rename extensions/crossmodel-lang/test/language-server/{utils => test-utils}/test-documents/relationship/index.ts (100%) rename extensions/crossmodel-lang/test/language-server/{utils => test-utils}/test-documents/relationship/relationship1.ts (100%) rename extensions/crossmodel-lang/test/language-server/{utils => test-utils}/test-documents/relationship/relationship2.ts (100%) rename extensions/crossmodel-lang/test/language-server/{utils => test-utils}/utils.ts (100%) diff --git a/.vscode/launch.json b/.vscode/launch.json index f0ec70c0..0894cd20 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -114,6 +114,20 @@ "${workspaceFolder}/node_modules/vscode-languageclient/**/*.js", "${workspaceFolder}/node_modules/vscode-jsonrpc/**/*.js" ] - } + }, + { + "name": "Debug Jest Tests", + "type": "node", + "request": "launch", + "runtimeArgs": [ + "--inspect-brk", + "${workspaceRoot}/node_modules/jest/bin/jest.js", + "--runInBand", + "--config=extensions/crossmodel-lang/jest.config.js", + "--testPathPattern=extensions/crossmodel-lang/test/language-server/serializer/cross-model-serializer.test.ts" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + } ] } diff --git a/extensions/crossmodel-lang/src/language-server/cross-model-serializer.ts b/extensions/crossmodel-lang/src/language-server/cross-model-serializer.ts index a9df8335..197bc16a 100644 --- a/extensions/crossmodel-lang/src/language-server/cross-model-serializer.ts +++ b/extensions/crossmodel-lang/src/language-server/cross-model-serializer.ts @@ -2,10 +2,10 @@ * Copyright (c) 2023 CrossBreeze. ********************************************************************************/ -import { AstNode, isReference } from 'langium'; +import { isReference } from 'langium'; import { Serializer } from '../model-server/serializer'; import { CrossModelServices } from './cross-model-module'; -import { CrossModelRoot } from './generated/ast'; +import { CrossModelRoot, Entity, Relationship, SystemDiagram } from './generated/ast'; /** * Hand-written AST serializer as there is currently no out-of-the box serializer from Langium, but it is on the roadmap. @@ -16,31 +16,15 @@ export class CrossModelSerializer implements Serializer { constructor(protected services: CrossModelServices, protected refNameProvider = services.references.QualifiedNameProvider) {} serialize(root: CrossModelRoot): string { - let newRoot: AstNode | undefined = this.toSerializableObject(root); - - let startKey; - - if (root.entity) { - startKey = 'entity'; - newRoot = root.entity; - } else if (root.diagram) { - startKey = 'diagram'; - newRoot = root.diagram; - } else if (root.relationship) { - startKey = 'relationship'; - newRoot = root.diagram; - } else { - return ''; - } - - return startKey + ':' + '\n ' + this.serializeValue(newRoot, 0); + const newRoot: CrossModelRoot | Entity | Relationship | SystemDiagram = this.toSerializableObject(root); + return this.serializeValue(newRoot, -4); } private serializeValue(value: any, indentationLevel: number): string { - if (typeof value === 'object' && value !== undefined) { + if (Array.isArray(value)) { + return this.serializeArray(value, indentationLevel); + } else if (typeof value === 'object' && value !== undefined) { return this.serializeObject(value, indentationLevel + 4); - } else if (Array.isArray(value)) { - return this.serializeArray(value, indentationLevel + 4); } else { return JSON.stringify(value); } @@ -49,19 +33,37 @@ export class CrossModelSerializer implements Serializer { private serializeObject(obj: Record, indentationLevel: number): string { const indentation = ' '.repeat(indentationLevel); - const serializedProperties = Object.entries(obj).map(([key, value]) => { - const serializedValue = this.serializeValue(value, indentationLevel); - return `${indentation}${key}: ${serializedValue}`; - }); - - return serializedProperties.join(',\n') + '\n'; + const serializedProperties = Object.entries(obj) + .map(([key, value]) => { + if (Array.isArray(value) && value.length === 0) { + return; + } + + const serializedValue = this.serializeValue(value, indentationLevel); + + if (key === 'name_val') { + key = 'name'; + } else if (key === 'name') { + key = 'id'; + } + + if (typeof value === 'object') { + return `${indentation}${key}:\n${serializedValue}`; + } else { + return `${indentation}${key}: ${serializedValue}`; + } + }) + .filter(item => item !== undefined); + + return serializedProperties.join('\n'); } private serializeArray(arr: any[], indentationLevel: number): string { - let serializedItems = arr.map(item => this.serializeValue(item, indentationLevel)).join('\n'); - serializedItems = this.changeCharInString(serializedItems, indentationLevel - 2, '-'); - - return serializedItems + '\n'; + const serializedItems = arr + .map(item => this.serializeValue(item, indentationLevel)) + .map(item => this.changeCharInString(item, indentationLevel + 2, '-')) + .join('\n'); + return serializedItems; } private changeCharInString(inputString: string, indexToChange: number, newChar: any): string { @@ -80,18 +82,20 @@ export class CrossModelSerializer implements Serializer { * @param obj semantic object * @returns serializable semantic object */ - toSerializableObject(obj?: T): T | undefined { - if (!obj) { - return; - } - + toSerializableObject(obj: T): T { return Object.entries(obj) .filter(([key, value]) => !key.startsWith('$')) .reduce((acc, [key, value]) => ({ ...acc, [key]: this.cleanValue(value) }), {}); } cleanValue(value: any): any { - return this.isContainedObject(value) ? this.toSerializableObject(value) : this.resolvedValue(value); + if (Array.isArray(value)) { + return value.map(item => this.cleanValue(item)); + } else if (this.isContainedObject(value)) { + return this.toSerializableObject(value); + } else { + return this.resolvedValue(value); + } } isContainedObject(value: any): boolean { diff --git a/extensions/crossmodel-lang/test/language-server/cross-model-lang-diagram.test.ts b/extensions/crossmodel-lang/test/language-server/cross-model-lang-diagram.test.ts index ade1f67f..8af73ac2 100644 --- a/extensions/crossmodel-lang/test/language-server/cross-model-lang-diagram.test.ts +++ b/extensions/crossmodel-lang/test/language-server/cross-model-lang-diagram.test.ts @@ -4,8 +4,8 @@ import { describe, expect, test } from '@jest/globals'; import { EmptyFileSystem, isReference } from 'langium'; -import { parseDocument } from './utils/utils'; -import { diagram1, diagram2, diagram3, diagram4, diagram5, diagram6 } from './utils/test-documents/diagram/index'; +import { parseDocument } from './test-utils/utils'; +import { diagram1, diagram2, diagram3, diagram4, diagram5, diagram6 } from './test-utils/test-documents/diagram/index'; import { CrossModelRoot } from '../../src/language-server/generated/ast'; import { createCrossModelServices } from '../../src/language-server/cross-model-module'; diff --git a/extensions/crossmodel-lang/test/language-server/cross-model-lang-entity.test.ts b/extensions/crossmodel-lang/test/language-server/cross-model-lang-entity.test.ts index 137d30ea..c9a2e7ac 100644 --- a/extensions/crossmodel-lang/test/language-server/cross-model-lang-entity.test.ts +++ b/extensions/crossmodel-lang/test/language-server/cross-model-lang-entity.test.ts @@ -5,8 +5,8 @@ import { describe, expect, test } from '@jest/globals'; import { EmptyFileSystem } from 'langium'; -import { parseDocument } from './utils/utils'; -import { entity1, entity2, entity3, entity4 } from './utils/test-documents/entity/index'; +import { parseDocument } from './test-utils/utils'; +import { entity1, entity2, entity3, entity4 } from './test-utils/test-documents/entity/index'; import { CrossModelRoot } from '../../src/language-server/generated/ast'; import { createCrossModelServices } from '../../src/language-server/cross-model-module'; diff --git a/extensions/crossmodel-lang/test/language-server/cross-model-lang-relationship.test.ts b/extensions/crossmodel-lang/test/language-server/cross-model-lang-relationship.test.ts index 54c8838a..567d96bc 100644 --- a/extensions/crossmodel-lang/test/language-server/cross-model-lang-relationship.test.ts +++ b/extensions/crossmodel-lang/test/language-server/cross-model-lang-relationship.test.ts @@ -5,8 +5,8 @@ import { describe, expect, test } from '@jest/globals'; import { EmptyFileSystem, isReference } from 'langium'; -import { parseDocument } from './utils/utils'; -import { relationship1, relationship2 } from './utils/test-documents/relationship/index'; +import { parseDocument } from './test-utils/utils'; +import { relationship1, relationship2 } from './test-utils/test-documents/relationship/index'; import { CrossModelRoot } from '../../src/language-server/generated/ast'; import { createCrossModelServices } from '../../src/language-server/cross-model-module'; diff --git a/extensions/crossmodel-lang/test/language-server/lexer/cross-model-token-generator.test.ts b/extensions/crossmodel-lang/test/language-server/lexer/cross-model-token-generator.test.ts index 77ccdea9..fbb47de8 100644 --- a/extensions/crossmodel-lang/test/language-server/lexer/cross-model-token-generator.test.ts +++ b/extensions/crossmodel-lang/test/language-server/lexer/cross-model-token-generator.test.ts @@ -5,10 +5,10 @@ import { describe, expect, test, beforeAll } from '@jest/globals'; import { Grammar } from 'langium'; -import { ExampleGrammarWithIndent } from '../utils/example-grammar'; +import { ExampleGrammarWithIndent } from '../test-utils/example-grammar'; import { TokenType } from 'chevrotain'; -import { ExampleGrammarWithNoIndent } from '../utils/example-grammar-no-indent'; +import { ExampleGrammarWithNoIndent } from '../test-utils/example-grammar-no-indent'; import { DEDENT, INDENT, NEWLINE, SPACES } from '../../../src/language-server/lexer/cross-model-indentation-tokens'; import { CrossModelTokenBuilder } from '../../../src/language-server/lexer/cross-model-token-generator'; diff --git a/extensions/crossmodel-lang/test/language-server/serializer/cross-model-serializer.test.ts b/extensions/crossmodel-lang/test/language-server/serializer/cross-model-serializer.test.ts new file mode 100644 index 00000000..c103552d --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/serializer/cross-model-serializer.test.ts @@ -0,0 +1,280 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ + +import { describe, test, beforeAll, expect } from '@jest/globals'; +import { EmptyFileSystem, Reference } from 'langium'; + +import { createCrossModelServices } from '../../../src/language-server/cross-model-module'; +import { CrossModelSerializer } from '../../../src/language-server/cross-model-serializer'; +import { CrossModelRoot, Entity, Relationship } from '../../../src/language-server/generated/ast'; +import _ from 'lodash'; + +const services = createCrossModelServices({ ...EmptyFileSystem }).CrossModel; + +describe('CrossModelLexer', () => { + let serializer: CrossModelSerializer; + + beforeAll(() => { + serializer = new CrossModelSerializer(services); + }); + + describe('Serialize entity', () => { + let crossModelRoot: CrossModelRoot; + let crossModelRootwithoutAttributes: CrossModelRoot; + let crossModelRootwithAttributesDifPlace: CrossModelRoot; + + beforeAll(() => { + crossModelRoot = { + $type: 'CrossModelRoot' + }; + + crossModelRootwithAttributesDifPlace = _.cloneDeep(crossModelRoot); + + crossModelRoot.entity = { + $container: crossModelRoot, + $type: 'Entity', + description: 'Test description', + name: 'test id', + name_val: 'test Name', + attributes: [] + }; + + crossModelRootwithoutAttributes = _.cloneDeep(crossModelRoot); + + crossModelRoot.entity.attributes = [ + { $container: crossModelRoot.entity, $type: 'EntityAttribute', name: 'Attribute 1', datatype: 'Datatype Attribute 1' }, + { $container: crossModelRoot.entity, $type: 'EntityAttribute', name: 'Attribute 2', datatype: 'Datatype Attribute 2' } + ]; + + crossModelRootwithAttributesDifPlace.entity = { + $container: crossModelRoot, + $type: 'Entity', + description: 'Test description', + attributes: [], + name: 'test id', + name_val: 'test Name' + }; + crossModelRootwithAttributesDifPlace.entity.attributes = [ + { $container: crossModelRoot.entity, $type: 'EntityAttribute', name: 'Attribute 1', datatype: 'Datatype Attribute 1' }, + { $container: crossModelRoot.entity, $type: 'EntityAttribute', name: 'Attribute 2', datatype: 'Datatype Attribute 2' } + ]; + }); + + test('serialize entity with attributes', () => { + const parseResult = serializer.serialize(crossModelRoot); + expect(parseResult).toBe(expected_result); + }); + + test('serialize entity without attributes', () => { + const parseResult = serializer.serialize(crossModelRootwithoutAttributes); + + expect(parseResult).toBe(expected_result2); + }); + + test('serialize entity with attributes in different place', () => { + const parseResult = serializer.serialize(crossModelRootwithAttributesDifPlace); + + expect(parseResult).toBe(expected_result3); + }); + }); + + describe('Serialize relationship', () => { + let crossModelRoot: CrossModelRoot; + + beforeAll(() => { + crossModelRoot = { + $type: 'CrossModelRoot' + }; + + const ref1: Reference = { + $refText: 'Ref1', + ref: { + $container: crossModelRoot, + $type: 'Entity', + description: 'Test description', + attributes: [], + name: 'Ref1', + name_val: 'test Name' + } + }; + + const ref2: Reference = { + $refText: 'Ref2', + ref: { + $container: crossModelRoot, + $type: 'Entity', + description: 'Test description', + attributes: [], + name: 'Ref2', + name_val: 'test Name' + } + }; + + crossModelRoot.relationship = { + $container: crossModelRoot, + $type: 'Relationship', + description: 'Test description', + name: 'test id', + name_val: 'test Name', + parent: ref1, + child: ref2, + type: 'n:m' + }; + }); + + test('serialize entity with attributes', () => { + const parseResult = serializer.serialize(crossModelRoot); + expect(parseResult).toBe(expected_result4); + }); + }); + + describe('Serialize diagram', () => { + let crossModelRoot: CrossModelRoot; + + beforeAll(() => { + crossModelRoot = { + $type: 'CrossModelRoot' + }; + + const ref1: Reference = { + $refText: 'Ref1', + ref: { + $container: crossModelRoot, + $type: 'Entity', + description: 'Test description', + attributes: [], + name: 'Ref1', + name_val: 'test Name' + } + }; + + const ref2: Reference = { + $refText: 'Ref2', + ref: { + $container: crossModelRoot, + $type: 'Entity', + description: 'Test description', + attributes: [], + name: 'Ref2', + name_val: 'test Name' + } + }; + + const ref3: Reference = { + $refText: 'Ref3', + ref: { + $container: crossModelRoot, + $type: 'Relationship', + description: 'Test description', + name: 'test id', + name_val: 'test Name', + parent: ref1, + child: ref2, + type: 'n:m' + } + }; + + crossModelRoot.diagram = { + $container: crossModelRoot, + $type: 'SystemDiagram', + description: 'Test description', + name: 'test id', + name_val: 'test Name', + nodes: [], + edges: [] + }; + + crossModelRoot.diagram.nodes = [ + { + $container: crossModelRoot.diagram, + $type: 'DiagramNode', + x: 100, + y: 101, + width: 102, + height: 102, + for: ref1, + name: 'Node1', + name_val: 'Node 1' + }, + { + $container: crossModelRoot.diagram, + $type: 'DiagramNode', + x: 100, + y: 101, + width: 102, + height: 102, + for: ref2, + name: 'Node2', + name_val: 'Node 2' + } + ]; + + crossModelRoot.diagram.edges = [ + { + $container: crossModelRoot.diagram, + $type: 'DiagramEdge', + for: ref3, + name: 'Edge1' + } + ]; + }); + + test('serialize entity with attributes', () => { + const parseResult = serializer.serialize(crossModelRoot); + expect(parseResult).toBe(expected_result5); + }); + }); +}); + +const expected_result = `entity: + description: "Test description" + id: "test id" + name: "test Name" + attributes: + - id: "Attribute 1" + datatype: "Datatype Attribute 1" + - id: "Attribute 2" + datatype: "Datatype Attribute 2"`; +const expected_result2 = `entity: + description: "Test description" + id: "test id" + name: "test Name"`; +const expected_result3 = `entity: + description: "Test description" + attributes: + - id: "Attribute 1" + datatype: "Datatype Attribute 1" + - id: "Attribute 2" + datatype: "Datatype Attribute 2" + id: "test id" + name: "test Name"`; +const expected_result4 = `relationship: + description: "Test description" + id: "test id" + name: "test Name" + parent: "Ref1" + child: "Ref2" + type: "n:m"`; +const expected_result5 = `diagram: + description: "Test description" + id: "test id" + name: "test Name" + nodes: + - x: 100 + y: 101 + width: 102 + height: 102 + for: "Ref1" + id: "Node1" + name: "Node 1" + - x: 100 + y: 101 + width: 102 + height: 102 + for: "Ref2" + id: "Node2" + name: "Node 2" + edges: + - for: "Ref3" + id: "Edge1"`; diff --git a/extensions/crossmodel-lang/test/language-server/utils/example-grammar-no-indent.ts b/extensions/crossmodel-lang/test/language-server/test-utils/example-grammar-no-indent.ts similarity index 100% rename from extensions/crossmodel-lang/test/language-server/utils/example-grammar-no-indent.ts rename to extensions/crossmodel-lang/test/language-server/test-utils/example-grammar-no-indent.ts diff --git a/extensions/crossmodel-lang/test/language-server/utils/example-grammar.ts b/extensions/crossmodel-lang/test/language-server/test-utils/example-grammar.ts similarity index 100% rename from extensions/crossmodel-lang/test/language-server/utils/example-grammar.ts rename to extensions/crossmodel-lang/test/language-server/test-utils/example-grammar.ts diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram1.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram1.ts similarity index 100% rename from extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram1.ts rename to extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram1.ts diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram2.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram2.ts similarity index 100% rename from extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram2.ts rename to extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram2.ts diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram3.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram3.ts similarity index 100% rename from extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram3.ts rename to extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram3.ts diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram4.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram4.ts similarity index 100% rename from extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram4.ts rename to extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram4.ts diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram5.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram5.ts similarity index 100% rename from extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram5.ts rename to extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram5.ts diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram6.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram6.ts similarity index 100% rename from extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram6.ts rename to extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram6.ts diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/index.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/index.ts similarity index 100% rename from extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/index.ts rename to extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/index.ts diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity1.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/entity/entity1.ts similarity index 100% rename from extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity1.ts rename to extensions/crossmodel-lang/test/language-server/test-utils/test-documents/entity/entity1.ts diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity2.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/entity/entity2.ts similarity index 100% rename from extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity2.ts rename to extensions/crossmodel-lang/test/language-server/test-utils/test-documents/entity/entity2.ts diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity3.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/entity/entity3.ts similarity index 100% rename from extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity3.ts rename to extensions/crossmodel-lang/test/language-server/test-utils/test-documents/entity/entity3.ts diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity4.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/entity/entity4.ts similarity index 100% rename from extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity4.ts rename to extensions/crossmodel-lang/test/language-server/test-utils/test-documents/entity/entity4.ts diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/index.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/entity/index.ts similarity index 100% rename from extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/index.ts rename to extensions/crossmodel-lang/test/language-server/test-utils/test-documents/entity/index.ts diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/relationship/index.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/relationship/index.ts similarity index 100% rename from extensions/crossmodel-lang/test/language-server/utils/test-documents/relationship/index.ts rename to extensions/crossmodel-lang/test/language-server/test-utils/test-documents/relationship/index.ts diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/relationship/relationship1.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/relationship/relationship1.ts similarity index 100% rename from extensions/crossmodel-lang/test/language-server/utils/test-documents/relationship/relationship1.ts rename to extensions/crossmodel-lang/test/language-server/test-utils/test-documents/relationship/relationship1.ts diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/relationship/relationship2.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/relationship/relationship2.ts similarity index 100% rename from extensions/crossmodel-lang/test/language-server/utils/test-documents/relationship/relationship2.ts rename to extensions/crossmodel-lang/test/language-server/test-utils/test-documents/relationship/relationship2.ts diff --git a/extensions/crossmodel-lang/test/language-server/utils/utils.ts b/extensions/crossmodel-lang/test/language-server/test-utils/utils.ts similarity index 100% rename from extensions/crossmodel-lang/test/language-server/utils/utils.ts rename to extensions/crossmodel-lang/test/language-server/test-utils/utils.ts diff --git a/package.json b/package.json index 172fa706..ea5f0f2d 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "start:browser": "yarn theia:browser start", "start:electron": "yarn theia:electron start", "start:verdaccio": "yarn verdaccio --config verdaccio-config.yaml", - "test": "lerna run test && vitest --config configs/vitest.config.ts", "theia:browser": "yarn --cwd applications/browser-app", "theia:electron": "yarn --cwd applications/electron-app", "watch": "lerna run --parallel watch" From af4971485ad231d846f74d342dfec477db1525c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96zcan=20Seker?= Date: Fri, 18 Aug 2023 12:42:22 +0200 Subject: [PATCH 06/22] GModel-factory now works with new lang --- .../source/voorbeeld_taal_diagram.diagram.cm | 34 +++++++++---------- .../model/cross-model-gmodel-factory.ts | 16 +++++---- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/examples/yaml-example/source/voorbeeld_taal_diagram.diagram.cm b/examples/yaml-example/source/voorbeeld_taal_diagram.diagram.cm index c5cb9c93..3941892b 100644 --- a/examples/yaml-example/source/voorbeeld_taal_diagram.diagram.cm +++ b/examples/yaml-example/source/voorbeeld_taal_diagram.diagram.cm @@ -1,21 +1,19 @@ diagram: id: "Systemdiagram1" + edges: + - id: "OrderCustomerEdge" + for: "Order_Customer" nodes: - - id: 'CustomerNode' - for: 'Customer' - x: 100 - y: 100 - height: 100 - width: 100 - - id: 'OrderNode' - for: 'Order' - x: 100 - y: 100 - height: 100 - width: 100 - edges: - - id: 'OrderCustomerEdge' - for: 'Order_Customer' - description: "This is also a test" - description: "This should be the description" - + - id: "CustomerNode" + for: "Customer" + x: 90 + y: 107 + height: 151 + width: 122.22364807128906 + - id: "OrderNode" + for: "Order" + x: 358 + y: 112 + height: 132 + width: 139.6079559326172 + description: "This should be the description" \ No newline at end of file diff --git a/extensions/crossmodel-lang/src/glsp-server/model/cross-model-gmodel-factory.ts b/extensions/crossmodel-lang/src/glsp-server/model/cross-model-gmodel-factory.ts index 86f051c8..a077a5cf 100644 --- a/extensions/crossmodel-lang/src/glsp-server/model/cross-model-gmodel-factory.ts +++ b/extensions/crossmodel-lang/src/glsp-server/model/cross-model-gmodel-factory.ts @@ -1,7 +1,7 @@ /******************************************************************************** * Copyright (c) 2023 CrossBreeze. ********************************************************************************/ -import { GEdge, GGraph, GLabel, GModelFactory, GNode } from '@eclipse-glsp/server'; +import { GEdge, GGraph, GModelFactory, GNode } from '@eclipse-glsp/server'; import { inject, injectable } from 'inversify'; import { DiagramEdge, DiagramNode } from '../../language-server/generated/ast'; import { CrossModelState } from './cross-model-state'; @@ -26,10 +26,10 @@ export class CrossModelGModelFactory implements GModelFactory { protected createGraph(): GGraph | undefined { const diagramRoot = this.modelState.diagramRoot; - const graphBuilder = GGraph.builder().id(this.modelState.semanticUri); + diagramRoot.nodes.map(node => this.createDiagramNode(node)).forEach(node => graphBuilder.add(node)); - // diagramRoot.edges.map(edge => this.createDiagramEdge(edge)).forEach(edge => graphBuilder.add(edge)); + diagramRoot.edges.map(edge => this.createDiagramEdge(edge, diagramRoot.nodes)).forEach(edge => graphBuilder.add(edge)); return graphBuilder.build(); } @@ -41,16 +41,20 @@ export class CrossModelGModelFactory implements GModelFactory { return GEntityNode.builder().id(id).addNode(node).build(); } - protected createDiagramEdge(edge: DiagramEdge): GEdge { + protected createDiagramEdge(edge: DiagramEdge, diagramNodes: DiagramNode[]): GEdge { const id = this.modelState.index.createId(edge) ?? 'unknown'; + const parentRef = edge.for?.ref?.parent?.$refText; const childRef = edge.for?.ref?.child?.$refText; + const parentDiagramNode = diagramNodes.find(item => item.for?.ref?.name === parentRef)?.name; + const childDiagramNode = diagramNodes.find(item => item.for?.ref?.name === childRef)?.name; + return GEdge.builder() .id(id) .addCssClasses('diagram-edge', 'relationship') - .sourceId(parentRef || '') - .targetId(childRef || '') + .sourceId(parentDiagramNode || '') + .targetId(childDiagramNode || '') .build(); } } From 6348a61a8afdd9eef4dfce217bf30be82a5db68d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96zcan=20Seker?= Date: Mon, 21 Aug 2023 10:41:34 +0200 Subject: [PATCH 07/22] Extension should be working with new language --- ...oorbeeld_taal_relationship.relationship.cm | 2 +- .../source/voorbeeld_taal_diagram.diagram.cm | 28 ++-- .../add-entity-action-provider.ts | 42 +++--- .../handler/add-entity-operation-handler.ts | 1 + .../handler/create-edge-operation-handler.ts | 1 + .../src/language-server/cross-model.langium | 6 +- .../src/language-server/generated/ast.ts | 4 +- .../src/language-server/generated/grammar.ts | 128 +++++++----------- .../src/language-server/util/ast-util.ts | 18 +-- .../syntaxes/cross-model.tmLanguage.json | 2 +- 10 files changed, 99 insertions(+), 133 deletions(-) diff --git a/examples/yaml-example/relationships/voorbeeld_taal_relationship.relationship.cm b/examples/yaml-example/relationships/voorbeeld_taal_relationship.relationship.cm index e2731216..2e5ef342 100644 --- a/examples/yaml-example/relationships/voorbeeld_taal_relationship.relationship.cm +++ b/examples/yaml-example/relationships/voorbeeld_taal_relationship.relationship.cm @@ -2,4 +2,4 @@ relationship: id: 'Order_Customer' parent: 'Customer' child: 'Order' - type: 1:1 \ No newline at end of file + type: "1:1" \ No newline at end of file diff --git a/examples/yaml-example/source/voorbeeld_taal_diagram.diagram.cm b/examples/yaml-example/source/voorbeeld_taal_diagram.diagram.cm index 3941892b..b4b26832 100644 --- a/examples/yaml-example/source/voorbeeld_taal_diagram.diagram.cm +++ b/examples/yaml-example/source/voorbeeld_taal_diagram.diagram.cm @@ -1,19 +1,19 @@ diagram: - id: "Systemdiagram1" + description: "This should be the description" edges: - - id: "OrderCustomerEdge" - for: "Order_Customer" + - for: "Order_Customer" + id: "OrderCustomerEdge" nodes: - - id: "CustomerNode" - for: "Customer" - x: 90 - y: 107 + - width: 122.22364807128906 height: 151 - width: 122.22364807128906 - - id: "OrderNode" - for: "Order" - x: 358 - y: 112 + y: 262.8195919791379 + x: 322.5893891316664 + for: "Customer" + id: "CustomerNode" + - width: 139.6079559326172 height: 132 - width: 139.6079559326172 - description: "This should be the description" \ No newline at end of file + y: 270.85224527770106 + x: 655.4416344093675 + for: "Order" + id: "OrderNode" + id: "Systemdiagram1" \ No newline at end of file diff --git a/extensions/crossmodel-lang/src/glsp-server/command-palette/add-entity-action-provider.ts b/extensions/crossmodel-lang/src/glsp-server/command-palette/add-entity-action-provider.ts index 9dc10769..418d91d9 100644 --- a/extensions/crossmodel-lang/src/glsp-server/command-palette/add-entity-action-provider.ts +++ b/extensions/crossmodel-lang/src/glsp-server/command-palette/add-entity-action-provider.ts @@ -16,26 +16,28 @@ import { CrossModelState } from '../model/cross-model-state'; */ @injectable() export class CrossModelAddEntityActionProvider implements ContextActionsProvider { - contextId = 'command-palette'; + contextId = 'command-palette'; - @inject(CrossModelState) protected state: CrossModelState; + @inject(CrossModelState) protected state: CrossModelState; - async getActions(editorContext: EditorContext): Promise { - const scopeProvider = this.state.services.language.references.ScopeProvider; - const refInfo = createNodeToEntityReference(this.state.diagramRoot); - const actions: LabeledAction[] = []; - const scope = scopeProvider.getScope(refInfo); - const duplicateStore = new Set(); - scope.getAllElements().forEach(description => { - if (!duplicateStore.has(description.name) && !isExternalDescriptionForLocalPackage(description, this.state.packageId)) { - actions.push({ - label: description.name, - actions: [AddEntityOperation.create(description.name, editorContext.lastMousePosition || Point.ORIGIN)], - icon: codiconCSSString('inspect') - }); - duplicateStore.add(description.name); - } - }); - return actions; - } + async getActions(editorContext: EditorContext): Promise { + const scopeProvider = this.state.services.language.references.ScopeProvider; + const refInfo = createNodeToEntityReference(this.state.diagramRoot); + const actions: LabeledAction[] = []; + const scope = scopeProvider.getScope(refInfo); + const duplicateStore = new Set(); + + scope.getAllElements().forEach(description => { + if (!duplicateStore.has(description.name) && !isExternalDescriptionForLocalPackage(description, this.state.packageId)) { + actions.push({ + label: description.name, + actions: [AddEntityOperation.create(description.name, editorContext.lastMousePosition || Point.ORIGIN)], + icon: codiconCSSString('inspect') + }); + duplicateStore.add(description.name); + } + }); + + return actions; + } } diff --git a/extensions/crossmodel-lang/src/glsp-server/handler/add-entity-operation-handler.ts b/extensions/crossmodel-lang/src/glsp-server/handler/add-entity-operation-handler.ts index 077cd2eb..0befa0d3 100644 --- a/extensions/crossmodel-lang/src/glsp-server/handler/add-entity-operation-handler.ts +++ b/extensions/crossmodel-lang/src/glsp-server/handler/add-entity-operation-handler.ts @@ -29,6 +29,7 @@ export class CrossModelAddEntityOperationHandler extends OperationHandler { const refInfo = createNodeToEntityReference(container); const scope = this.state.services.language.references.ScopeProvider.getScope(refInfo); const entityDescription = scope.getElement(operation.entityName); + if (entityDescription) { // create node for entity const node: DiagramNode = { diff --git a/extensions/crossmodel-lang/src/glsp-server/handler/create-edge-operation-handler.ts b/extensions/crossmodel-lang/src/glsp-server/handler/create-edge-operation-handler.ts index 9623279d..4cb98914 100644 --- a/extensions/crossmodel-lang/src/glsp-server/handler/create-edge-operation-handler.ts +++ b/extensions/crossmodel-lang/src/glsp-server/handler/create-edge-operation-handler.ts @@ -39,6 +39,7 @@ export class CrossModelCreateEdgeOperationHandler extends OperationHandler imple protected async createEdge(operation: CreateEdgeOperation): Promise { const sourceNode = this.state.index.findDiagramNode(operation.sourceElementId); const targetNode = this.state.index.findDiagramNode(operation.targetElementId); + if (sourceNode && targetNode) { // before we can create a digram edge, we need to create the corresponding relationship that it is based on const relationship = await this.createAndSaveRelationship(sourceNode, targetNode); diff --git a/extensions/crossmodel-lang/src/language-server/cross-model.langium b/extensions/crossmodel-lang/src/language-server/cross-model.langium index 32d782b8..8661c305 100644 --- a/extensions/crossmodel-lang/src/language-server/cross-model.langium +++ b/extensions/crossmodel-lang/src/language-server/cross-model.langium @@ -58,14 +58,10 @@ RelationshipFields infers Relationship: 'description' ':' description=STRING | 'parent' ':' parent=[Entity:QualifiedName] | 'child' ':' child=[Entity:QualifiedName] | - 'type' ':' type=RelationshipType + 'type' ':' type=STRING ) ; -RelationshipType returns string: - '1:1' | '1:n' | 'n:1' | 'n:m' -; - // Diagram defintion SystemDiagram: 'diagram' ':' diff --git a/extensions/crossmodel-lang/src/language-server/generated/ast.ts b/extensions/crossmodel-lang/src/language-server/generated/ast.ts index 64f8e69e..6539adee 100644 --- a/extensions/crossmodel-lang/src/language-server/generated/ast.ts +++ b/extensions/crossmodel-lang/src/language-server/generated/ast.ts @@ -8,8 +8,6 @@ import { AstNode, AbstractAstReflection, Reference, ReferenceInfo, TypeMetaData export type QualifiedName = string; -export type RelationshipType = '1:1' | '1:n' | 'n:1' | 'n:m'; - export interface CrossModelRoot extends AstNode { readonly $type: 'CrossModelRoot'; diagram?: SystemDiagram @@ -93,7 +91,7 @@ export interface Relationship extends AstNode { name?: string name_val?: string parent?: Reference - type?: RelationshipType + type?: string } export const Relationship = 'Relationship'; diff --git a/extensions/crossmodel-lang/src/language-server/generated/grammar.ts b/extensions/crossmodel-lang/src/language-server/generated/grammar.ts index 4a670fb7..456ba440 100644 --- a/extensions/crossmodel-lang/src/language-server/generated/grammar.ts +++ b/extensions/crossmodel-lang/src/language-server/generated/grammar.ts @@ -49,7 +49,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@9" + "$ref": "#/rules@8" }, "arguments": [] } @@ -83,7 +83,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@23" + "$ref": "#/rules@22" }, "arguments": [] }, @@ -98,7 +98,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@22" + "$ref": "#/rules@21" }, "arguments": [] } @@ -142,7 +142,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@18" + "$ref": "#/rules@17" }, "arguments": [] } @@ -167,7 +167,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@18" + "$ref": "#/rules@17" }, "arguments": [] } @@ -192,7 +192,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@18" + "$ref": "#/rules@17" }, "arguments": [] } @@ -241,7 +241,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@23" + "$ref": "#/rules@22" }, "arguments": [] }, @@ -261,7 +261,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@22" + "$ref": "#/rules@21" }, "arguments": [] } @@ -329,7 +329,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@18" + "$ref": "#/rules@17" }, "arguments": [] } @@ -354,7 +354,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@18" + "$ref": "#/rules@17" }, "arguments": [] } @@ -379,7 +379,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@18" + "$ref": "#/rules@17" }, "arguments": [] } @@ -404,7 +404,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@18" + "$ref": "#/rules@17" }, "arguments": [] } @@ -440,7 +440,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@23" + "$ref": "#/rules@22" }, "arguments": [] }, @@ -455,7 +455,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@22" + "$ref": "#/rules@21" }, "arguments": [] } @@ -499,7 +499,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@18" + "$ref": "#/rules@17" }, "arguments": [] } @@ -524,7 +524,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@18" + "$ref": "#/rules@17" }, "arguments": [] } @@ -549,7 +549,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@18" + "$ref": "#/rules@17" }, "arguments": [] } @@ -579,7 +579,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@17" + "$ref": "#/rules@16" }, "arguments": [] }, @@ -611,7 +611,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@17" + "$ref": "#/rules@16" }, "arguments": [] }, @@ -638,7 +638,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@8" + "$ref": "#/rules@17" }, "arguments": [] } @@ -654,38 +654,6 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "parameters": [], "wildcard": false }, - { - "$type": "ParserRule", - "name": "RelationshipType", - "dataType": "string", - "definition": { - "$type": "Alternatives", - "elements": [ - { - "$type": "Keyword", - "value": "1:1" - }, - { - "$type": "Keyword", - "value": "1:n" - }, - { - "$type": "Keyword", - "value": "n:1" - }, - { - "$type": "Keyword", - "value": "n:m" - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, { "$type": "ParserRule", "name": "SystemDiagram", @@ -706,14 +674,14 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@23" + "$ref": "#/rules@22" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@10" + "$ref": "#/rules@9" }, "arguments": [], "cardinality": "*" @@ -721,7 +689,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@22" + "$ref": "#/rules@21" }, "arguments": [] } @@ -761,7 +729,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@11" + "$ref": "#/rules@10" }, "arguments": [] } @@ -781,7 +749,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@14" + "$ref": "#/rules@13" }, "arguments": [] } @@ -805,7 +773,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@18" + "$ref": "#/rules@17" }, "arguments": [] } @@ -830,7 +798,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@18" + "$ref": "#/rules@17" }, "arguments": [] } @@ -855,7 +823,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@18" + "$ref": "#/rules@17" }, "arguments": [] } @@ -884,7 +852,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@23" + "$ref": "#/rules@22" }, "arguments": [] }, @@ -895,7 +863,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@12" + "$ref": "#/rules@11" }, "arguments": [] }, @@ -904,7 +872,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@22" + "$ref": "#/rules@21" }, "arguments": [] } @@ -930,7 +898,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@13" + "$ref": "#/rules@12" }, "arguments": [], "cardinality": "*" @@ -972,7 +940,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@18" + "$ref": "#/rules@17" }, "arguments": [] } @@ -1002,7 +970,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@17" + "$ref": "#/rules@16" }, "arguments": [] }, @@ -1029,7 +997,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@19" + "$ref": "#/rules@18" }, "arguments": [] } @@ -1054,7 +1022,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@19" + "$ref": "#/rules@18" }, "arguments": [] } @@ -1079,7 +1047,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@19" + "$ref": "#/rules@18" }, "arguments": [] } @@ -1104,7 +1072,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@19" + "$ref": "#/rules@18" }, "arguments": [] } @@ -1129,7 +1097,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@18" + "$ref": "#/rules@17" }, "arguments": [] } @@ -1154,7 +1122,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@18" + "$ref": "#/rules@17" }, "arguments": [] } @@ -1183,7 +1151,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@23" + "$ref": "#/rules@22" }, "arguments": [] }, @@ -1194,7 +1162,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@15" + "$ref": "#/rules@14" }, "arguments": [] }, @@ -1203,7 +1171,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@22" + "$ref": "#/rules@21" }, "arguments": [] } @@ -1229,7 +1197,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@16" + "$ref": "#/rules@15" }, "arguments": [], "cardinality": "*" @@ -1276,7 +1244,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@17" + "$ref": "#/rules@16" }, "arguments": [] }, @@ -1303,7 +1271,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@18" + "$ref": "#/rules@17" }, "arguments": [] } @@ -1329,7 +1297,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@18" + "$ref": "#/rules@17" }, "arguments": [] }, @@ -1343,7 +1311,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@18" + "$ref": "#/rules@17" }, "arguments": [] } diff --git a/extensions/crossmodel-lang/src/language-server/util/ast-util.ts b/extensions/crossmodel-lang/src/language-server/util/ast-util.ts index b963efbe..14e3b517 100644 --- a/extensions/crossmodel-lang/src/language-server/util/ast-util.ts +++ b/extensions/crossmodel-lang/src/language-server/util/ast-util.ts @@ -5,13 +5,13 @@ import { Reference, ReferenceInfo } from 'langium'; import { DiagramNode, SystemDiagram } from '../generated/ast'; export function createNodeToEntityReference(root: SystemDiagram): ReferenceInfo { - return { - reference: {} as Reference, - container: { - $type: DiagramNode, - $container: root, - $containerProperty: 'nodes' - }, - property: 'semanticElement' - }; + return { + reference: {} as Reference, + container: { + $type: DiagramNode, + $container: root, + $containerProperty: 'nodes' + }, + property: 'for' + }; } diff --git a/extensions/crossmodel-lang/syntaxes/cross-model.tmLanguage.json b/extensions/crossmodel-lang/syntaxes/cross-model.tmLanguage.json index 21caaafa..4365b646 100644 --- a/extensions/crossmodel-lang/syntaxes/cross-model.tmLanguage.json +++ b/extensions/crossmodel-lang/syntaxes/cross-model.tmLanguage.json @@ -10,7 +10,7 @@ }, { "name": "keyword.control.cross-model", - "match": "\\b(1:n|attributes|child|datatype|description|diagram|edges|entity|for|height|id|n:1|n:m|name|nodes|parent|relationship|type|width|x|y)\\b" + "match": "\\b(attributes|child|datatype|description|diagram|edges|entity|for|height|id|name|nodes|parent|relationship|type|width|x|y)\\b" }, { "name": "string.quoted.double.cross-model", From 78f85bd90ccd70940f92119e7dd0deaf353db2cd Mon Sep 17 00:00:00 2001 From: Martin Fleck Date: Tue, 22 Aug 2023 12:30:00 +0200 Subject: [PATCH 08/22] Fix error about character needing to be non-negative caused by '-1' --- .../lexer/cross-model-lexer.ts | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/extensions/crossmodel-lang/src/language-server/lexer/cross-model-lexer.ts b/extensions/crossmodel-lang/src/language-server/lexer/cross-model-lexer.ts index 1aafb78d..e8f76c16 100644 --- a/extensions/crossmodel-lang/src/language-server/lexer/cross-model-lexer.ts +++ b/extensions/crossmodel-lang/src/language-server/lexer/cross-model-lexer.ts @@ -2,11 +2,11 @@ * Copyright (c) 2023 CrossBreeze. ********************************************************************************/ +import { createTokenInstance } from 'chevrotain'; import { DefaultLexer, LexerResult } from 'langium'; import { indentStack } from './cross-model-indent-stack'; -import { IndentationError } from './cross-model-lexer-error'; -import { createTokenInstance } from 'chevrotain'; import { DEDENT, NAMES } from './cross-model-indentation-tokens'; +import { IndentationError } from './cross-model-lexer-error'; /** * Custom CrossModelLexer to get indentation working. @@ -70,14 +70,21 @@ export class CrossModelLexer extends DefaultLexer { */ private createTrailingDedentTokens(text: string, lexingResult: LexerResult): void { // These are there to put the error warning in the right place in the editor - const newlines = text.split(/\r\n|\r|\n/).length; - let match = text.match(/.*$/)?.length; - match = match ? match - 1 : 0; + const lines = text.split(/\r\n|\r|\n/); + const lastLine = lines[lines.length - 1]; - // add remaining Outdents - while (indentStack.length() > 1) { - lexingResult.tokens.push(createTokenInstance(DEDENT, NAMES.DEDENT, text.length, text.length, newlines, newlines, match, match)); - indentStack.pop(); + // add remaining dedents + while (indentStack.pop()) { + // chevrotrain uses 1-based indices for tokens which Langium transforms into 0-based indices by deducting 1 + // see for instance https://github.com/eclipse-langium/langium/blob/eea5bc2/packages/langium/src/utils/cst-util.ts#L49 + const startOffset = text.length || 1; + const endOffset = text.length || 1; + const startLine = lines.length || 1; + const endLine = lines.length || 1; + const startColumn = lastLine?.length || 0 + 1; + const endColumn = lastLine?.length || 0; // for some reason end-column uses the correct index + const lineToken = createTokenInstance(DEDENT, NAMES.DEDENT, startOffset, endOffset, startLine, endLine, startColumn, endColumn); + lexingResult.tokens.push(lineToken); } } } From 5e70933472ed41cd4531395b5957b15c695f82f2 Mon Sep 17 00:00:00 2001 From: Martin Fleck Date: Tue, 29 Aug 2023 14:20:11 +0200 Subject: [PATCH 09/22] Fix errors by using a specific chevrotain version - Downgrade from 10.5.0 to 10.4.2 as it does not work for our YAML lang -- https://github.com/Chevrotain/chevrotain/compare/v10.4.2...v10.5.0 -- Main extension: https://github.com/Chevrotain/chevrotain/pull/1917 --- extensions/crossmodel-lang/package.json | 10 +-- .../src/language-server/generated/ast.ts | 21 ++++- .../src/language-server/generated/grammar.ts | 11 +-- .../src/language-server/generated/module.ts | 8 +- yarn.lock | 80 ++++++++----------- 5 files changed, 66 insertions(+), 64 deletions(-) diff --git a/extensions/crossmodel-lang/package.json b/extensions/crossmodel-lang/package.json index 7d4e6884..ca9451c9 100644 --- a/extensions/crossmodel-lang/package.json +++ b/extensions/crossmodel-lang/package.json @@ -93,10 +93,10 @@ "@crossbreeze/protocol": "0.0.0", "@eclipse-glsp/layout-elk": "1.1.0-RC06", "@eclipse-glsp/server": "1.1.0-RC06", - "chalk": "^4.1.2", - "chevrotain": "^10.4.1", - "commander": "^8.0.0", - "langium": "^1.1.0", + "chalk": "~4.1.2", + "chevrotain": "~10.4.2", + "commander": "~10.0.0", + "langium": "~1.3.0", "type-fest": "^3.6.1", "vscode-languageclient": "8.0.2", "vscode-languageserver": "8.0.2", @@ -111,7 +111,7 @@ "@typescript-eslint/parser": "^5.28.0", "@vscode/vsce": "^2.17.0", "eslint": "^8.17.0", - "langium-cli": "^1.1.0", + "langium-cli": "~1.3.0", "ts-loader": "^9.4.2", "typescript": "^4.9.4" }, diff --git a/extensions/crossmodel-lang/src/language-server/generated/ast.ts b/extensions/crossmodel-lang/src/language-server/generated/ast.ts index 6539adee..ceb5abe0 100644 --- a/extensions/crossmodel-lang/src/language-server/generated/ast.ts +++ b/extensions/crossmodel-lang/src/language-server/generated/ast.ts @@ -1,13 +1,28 @@ /****************************************************************************** - * This file was generated by langium-cli 1.1.0. + * This file was generated by langium-cli 1.3.1. * DO NOT EDIT MANUALLY! ******************************************************************************/ /* eslint-disable */ -import { AstNode, AbstractAstReflection, Reference, ReferenceInfo, TypeMetaData } from 'langium'; +import type { AstNode, Reference, ReferenceInfo, TypeMetaData } from 'langium'; +import { AbstractAstReflection } from 'langium'; + +export const CrossModelTerminals = { + STRING: /"[^"]*"|'[^']*'/, + NUMBER: /(-)?[0-9]+(\.[0-9]*)?/, + SL_COMMENT: /#[^\n\r]*/, + NEWLINE: /this_string_does_not_matter_newline#\$%\^&\*\(\(/, + DEDENT: /this_string_does_not_matter_dedent#\$%\^&\*\(\(/, + INDENT: /this_string_does_not_matter_indent#\$%\^&\*\(\(/, + SPACES: /this_string_does_not_matter_spaces#\$%\^&\*\(\(/, +}; export type QualifiedName = string; +export function isQualifiedName(item: unknown): item is QualifiedName { + return typeof item === 'string'; +} + export interface CrossModelRoot extends AstNode { readonly $type: 'CrossModelRoot'; diagram?: SystemDiagram @@ -116,7 +131,7 @@ export function isSystemDiagram(item: unknown): item is SystemDiagram { return reflection.isInstance(item, SystemDiagram); } -export interface CrossModelAstType { +export type CrossModelAstType = { CrossModelRoot: CrossModelRoot DiagramEdge: DiagramEdge DiagramNode: DiagramNode diff --git a/extensions/crossmodel-lang/src/language-server/generated/grammar.ts b/extensions/crossmodel-lang/src/language-server/generated/grammar.ts index 456ba440..262c4711 100644 --- a/extensions/crossmodel-lang/src/language-server/generated/grammar.ts +++ b/extensions/crossmodel-lang/src/language-server/generated/grammar.ts @@ -1,9 +1,10 @@ /****************************************************************************** - * This file was generated by langium-cli 1.1.0. + * This file was generated by langium-cli 1.3.1. * DO NOT EDIT MANUALLY! ******************************************************************************/ -import { loadGrammarFromJson, Grammar } from 'langium'; +import type { Grammar } from 'langium'; +import { loadGrammarFromJson } from 'langium'; let loadedCrossModelGrammar: Grammar | undefined; export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (loadedCrossModelGrammar = loadGrammarFromJson(`{ @@ -1332,7 +1333,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "name": "STRING", "definition": { "$type": "RegexToken", - "regex": "\\"[^\\"]*\\"|'[^']*'" + "regex": "/\\"[^\\"]*\\"|'[^']*'/" }, "fragment": false, "hidden": false @@ -1346,7 +1347,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load }, "definition": { "$type": "RegexToken", - "regex": "(-)?[0-9]+(\\\\.[0-9]*)?" + "regex": "/(-)?[0-9]+(\\\\.[0-9]*)?/" }, "fragment": false, "hidden": false @@ -1357,7 +1358,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "name": "SL_COMMENT", "definition": { "$type": "RegexToken", - "regex": "#[^\\\\n\\\\r]*" + "regex": "/#[^\\\\n\\\\r]*/" }, "fragment": false }, diff --git a/extensions/crossmodel-lang/src/language-server/generated/module.ts b/extensions/crossmodel-lang/src/language-server/generated/module.ts index d9328c05..ea1a4391 100644 --- a/extensions/crossmodel-lang/src/language-server/generated/module.ts +++ b/extensions/crossmodel-lang/src/language-server/generated/module.ts @@ -1,17 +1,17 @@ /****************************************************************************** - * This file was generated by langium-cli 1.1.0. + * This file was generated by langium-cli 1.3.1. * DO NOT EDIT MANUALLY! ******************************************************************************/ -import { LangiumGeneratedServices, LangiumGeneratedSharedServices, LangiumSharedServices, LangiumServices, LanguageMetaData, Module } from 'langium'; +import type { LangiumGeneratedServices, LangiumGeneratedSharedServices, LangiumSharedServices, LangiumServices, LanguageMetaData, Module } from 'langium'; import { CrossModelAstReflection } from './ast'; import { CrossModelGrammar } from './grammar'; -export const CrossModelLanguageMetaData: LanguageMetaData = { +export const CrossModelLanguageMetaData = { languageId: 'cross-model', fileExtensions: ['.cm'], caseInsensitive: false -}; +} as const satisfies LanguageMetaData; export const CrossModelGeneratedSharedModule: Module = { AstReflection: () => new CrossModelAstReflection() diff --git a/yarn.lock b/yarn.lock index 39dad43c..85b0124c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -979,15 +979,6 @@ "@chevrotain/types" "10.4.2" lodash "4.17.21" -"@chevrotain/cst-dts-gen@10.5.0": - version "10.5.0" - resolved "https://registry.yarnpkg.com/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz#922ebd8cc59d97241bb01b1b17561a5c1ae0124e" - integrity sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw== - dependencies: - "@chevrotain/gast" "10.5.0" - "@chevrotain/types" "10.5.0" - lodash "4.17.21" - "@chevrotain/gast@10.4.2": version "10.4.2" resolved "https://registry.yarnpkg.com/@chevrotain/gast/-/gast-10.4.2.tgz#236dc48e54cba16260c03bece25d5a3b6e2f5dab" @@ -996,34 +987,16 @@ "@chevrotain/types" "10.4.2" lodash "4.17.21" -"@chevrotain/gast@10.5.0": - version "10.5.0" - resolved "https://registry.yarnpkg.com/@chevrotain/gast/-/gast-10.5.0.tgz#e4e614bc46d17a8892742f38e56cd33f1f3ad162" - integrity sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A== - dependencies: - "@chevrotain/types" "10.5.0" - lodash "4.17.21" - "@chevrotain/types@10.4.2": version "10.4.2" resolved "https://registry.yarnpkg.com/@chevrotain/types/-/types-10.4.2.tgz#18be6b7a3226b121fccec08c2ba8433219a6813c" integrity sha512-QzSCjg6G4MvIoLeIgOiMR0IgzkGEQqrNJJIr3T5ETRa7l4Av4AMIiEctV99mvDr57iXwwk0/kr3RJxiU36Nevw== -"@chevrotain/types@10.5.0": - version "10.5.0" - resolved "https://registry.yarnpkg.com/@chevrotain/types/-/types-10.5.0.tgz#52a97d74a8cfbc197f054636d93ecd8912d33d21" - integrity sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A== - "@chevrotain/utils@10.4.2": version "10.4.2" resolved "https://registry.yarnpkg.com/@chevrotain/utils/-/utils-10.4.2.tgz#87735732184cc5a2f8aad2f3454082294ef3c924" integrity sha512-V34dacxWLwKcvcy32dx96ADJVdB7kOJLm7LyBkBQw5u5HC9WdEFw2G17zml+U3ivavGTrGPJHl8o9/UJm0PlUw== -"@chevrotain/utils@10.5.0": - version "10.5.0" - resolved "https://registry.yarnpkg.com/@chevrotain/utils/-/utils-10.5.0.tgz#0ee36f65b49b447fbac71b9e5af5c5c6c98ac057" - integrity sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ== - "@cnakazawa/watch@^1.0.3": version "1.0.4" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a" @@ -5074,7 +5047,7 @@ chalk@^2.0.0, chalk@^2.1.0, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2, chalk@~4.1.2: +chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@~4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -5129,18 +5102,6 @@ chevrotain-allstar@~0.1.4: dependencies: lodash "^4.17.21" -chevrotain@^10.4.1: - version "10.5.0" - resolved "https://registry.yarnpkg.com/chevrotain/-/chevrotain-10.5.0.tgz#9c1dc62ef0753bb562dbe521b5f72d041bad624e" - integrity sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A== - dependencies: - "@chevrotain/cst-dts-gen" "10.5.0" - "@chevrotain/gast" "10.5.0" - "@chevrotain/types" "10.5.0" - "@chevrotain/utils" "10.5.0" - lodash "4.17.21" - regexp-to-ast "0.5.0" - chevrotain@~10.4.2: version "10.4.2" resolved "https://registry.yarnpkg.com/chevrotain/-/chevrotain-10.4.2.tgz#9abeac6a60134931c0a0788b206400e5f7a3daba" @@ -5477,7 +5438,7 @@ commander@^7.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== -commander@^8.0.0, commander@^8.3.0: +commander@^8.3.0: version "8.3.0" resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== @@ -9920,19 +9881,28 @@ kuler@^2.0.0: resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== -langium-cli@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/langium-cli/-/langium-cli-1.1.0.tgz#3bc778f32e4324b4666a5f55a5b5546965bf2e12" - integrity sha512-vnv037FHqXqMeNiNF90v47VrJGiJPzH721UIbbHcu6Nfx0C1UC6SmQhGHtZIDRovT5qJsiXRIPDTZYrIkm4KJQ== +langium-cli@~1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/langium-cli/-/langium-cli-1.3.1.tgz#d73777fbf866d429e242b4bf7676d1251aafa994" + integrity sha512-9faKpioKCjBD0Z4y165+wQlDFiDHOXYBlhPVgbV+neSnSB70belZLNfykAVa564360h7Br/5PogR5jW2n/tOKw== dependencies: chalk "~4.1.2" commander "~10.0.0" fs-extra "~11.1.0" jsonschema "~1.4.1" - langium "~1.1.0" + langium "~1.3.0" + langium-railroad "~1.3.0" lodash "~4.17.21" -langium@^1.1.0, langium@~1.1.0: +langium-railroad@~1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/langium-railroad/-/langium-railroad-1.3.0.tgz#ef2bb50ac26811332c11ea9a62b4e66f96c3f7f4" + integrity sha512-I3gx79iF+Qpn2UjzfHLf2GENAD9mPdSZHL3juAZLBsxznw4se7MBrJX32oPr/35DTjU9q99wFCQoCXu7mcf+Bg== + dependencies: + langium "~1.3.0" + railroad-diagrams "^1.0.0" + +langium@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/langium/-/langium-1.1.0.tgz#b512ff8ae8d458a3b047233c31b41de9a969200c" integrity sha512-TsWY/DIOR73se9/YaMQZpvfFWWrhWP0FQS9MrpxWEnMJR0FoKVpMF1thPWXZexLSfyEm1pn2oYzCdW4KUBqXxA== @@ -9943,6 +9913,17 @@ langium@^1.1.0, langium@~1.1.0: vscode-languageserver-textdocument "~1.0.8" vscode-uri "~3.0.7" +langium@~1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/langium/-/langium-1.3.0.tgz#7d7c82466f2188f698f2583d9d841c8eaf679bc8" + integrity sha512-MjCEWu1q2TcuLURNjwA2StRgx6jGD+21fvzKDscSgD9lQC4Vmd4lkkgrKLakLrxcbJ57UtzUNfB6j/Yx9skVUw== + dependencies: + chevrotain "~10.4.2" + chevrotain-allstar "~0.1.4" + vscode-languageserver "~8.0.2" + vscode-languageserver-textdocument "~1.0.8" + vscode-uri "~3.0.7" + lazy-val@^1.0.4, lazy-val@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/lazy-val/-/lazy-val-1.0.5.tgz#6cf3b9f5bc31cee7ee3e369c0832b7583dcd923d" @@ -12241,6 +12222,11 @@ quick-lru@^5.1.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== +railroad-diagrams@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e" + integrity sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A== + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" From 4bd47ab18d9eb4455ba397f42a6acfc8a2013b26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96zcan=20Seker?= Date: Fri, 1 Sep 2023 15:31:06 +0200 Subject: [PATCH 10/22] To get it working on my machine --- packages/protocol/package.json | 2 +- yarn.lock | 11 ----------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/packages/protocol/package.json b/packages/protocol/package.json index e1f3adc1..fc275ed8 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -29,6 +29,6 @@ "dependencies": { "@eclipse-glsp/protocol": "1.1.0-RC06", "vscode-jsonrpc": "^8.0.2", - "langium": "^1.1.0" + "langium": "~1.3.0" } } diff --git a/yarn.lock b/yarn.lock index 85b0124c..53bb50cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9902,17 +9902,6 @@ langium-railroad@~1.3.0: langium "~1.3.0" railroad-diagrams "^1.0.0" -langium@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/langium/-/langium-1.1.0.tgz#b512ff8ae8d458a3b047233c31b41de9a969200c" - integrity sha512-TsWY/DIOR73se9/YaMQZpvfFWWrhWP0FQS9MrpxWEnMJR0FoKVpMF1thPWXZexLSfyEm1pn2oYzCdW4KUBqXxA== - dependencies: - chevrotain "~10.4.2" - chevrotain-allstar "~0.1.4" - vscode-languageserver "~8.0.2" - vscode-languageserver-textdocument "~1.0.8" - vscode-uri "~3.0.7" - langium@~1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/langium/-/langium-1.3.0.tgz#7d7c82466f2188f698f2583d9d841c8eaf679bc8" From 27bf044466a9eeece64e97b58e49e67fa9a4b7c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96zcan=20Seker?= Date: Fri, 1 Sep 2023 15:31:31 +0200 Subject: [PATCH 11/22] readme update for license --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index afd34909..6f898139 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,8 @@ The repository itself contains the following components structured as follows: ## Used libraries and license -- mui-x : MUI X is open core—base components are MIT-licensed, while more advanced features require a Pro or Premium commercial license. We are - currently only using core-base +- mui-x : MUI X is open core—base components are MIT-licensed, while more advanced features require a Pro or Premium commercial license. We are currently only using core-base - react-tabs: MIT +- chevotain: This library is a dependency of langium. To get the Yaml language working in crossmodel an example implementation of the python language of chevrotrain has been used. This example has been modified to make it work for the yaml language. + - https://github.com/Chevrotain/chevrotain + - example that has been used: https://github.com/Chevrotain/chevrotain/tree/master/examples/lexer/python_indentation From 19d779bd445f9a8cc79f9c56c130a3d70088c704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96zcan=20Seker?= Date: Fri, 1 Sep 2023 16:08:51 +0200 Subject: [PATCH 12/22] fixing tests --- .vscode/launch.json | 2 +- .../language-server/util/name-util.test.ts | 58 - .../lexer/cross-model-token-generator.test.ts | 29 +- .../test-utils/example-grammar-no-indent.ts | 1343 ---------------- .../test-utils/example-grammar.ts | 1356 ----------------- .../relationship/relationship1.ts | 2 +- .../relationship/relationship2.ts | 2 +- .../language-server/util/name-util.test.ts | 58 + package.json | 1 + 9 files changed, 79 insertions(+), 2772 deletions(-) delete mode 100644 extensions/crossmodel-lang/src/language-server/util/name-util.test.ts delete mode 100644 extensions/crossmodel-lang/test/language-server/test-utils/example-grammar-no-indent.ts delete mode 100644 extensions/crossmodel-lang/test/language-server/test-utils/example-grammar.ts create mode 100644 extensions/crossmodel-lang/test/language-server/util/name-util.test.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index 0894cd20..b8b55b41 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -124,7 +124,7 @@ "${workspaceRoot}/node_modules/jest/bin/jest.js", "--runInBand", "--config=extensions/crossmodel-lang/jest.config.js", - "--testPathPattern=extensions/crossmodel-lang/test/language-server/serializer/cross-model-serializer.test.ts" + "--testPathPattern=extensions/crossmodel-lang/test/language-server/util/name-util.test.ts" ], "console": "integratedTerminal", "internalConsoleOptions": "neverOpen" diff --git a/extensions/crossmodel-lang/src/language-server/util/name-util.test.ts b/extensions/crossmodel-lang/src/language-server/util/name-util.test.ts deleted file mode 100644 index 3a06ff4f..00000000 --- a/extensions/crossmodel-lang/src/language-server/util/name-util.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2023 CrossBreeze. - ********************************************************************************/ -import assert from 'assert'; -import { EmptyFileSystem } from 'langium'; -import { parseDocument } from 'langium/test'; -import 'mocha'; -import 'reflect-metadata'; -import { describe, test } from 'vitest'; -import { createCrossModelServices } from '../cross-model-module'; -import { CrossModelRoot } from '../generated/ast'; -import { findAvailableNodeName } from './name-util'; - -const services = createCrossModelServices({ ...EmptyFileSystem }); -const cmServices = services.CrossModel; - -describe('NameUtil', () => { - describe('findAvailableNodeName', () => { - test('should return given name if unique', async () => { - const document = await parseDocument(cmServices, 'diagram {}'); - assert.strictEqual(findAvailableNodeName(document.parseResult.value.diagram!, 'nodeA'), 'nodeA'); - }); - - test('should return unique name if given is taken', async () => { - const document = await parseDocument( - cmServices, - `diagram { - node nodeA for A { x := 10; y := 10; width := 10; height := 10; }; - }` - ); - assert.strictEqual(findAvailableNodeName(document.parseResult.value.diagram!, 'nodeA'), 'nodeA1'); - }); - - test('should properly count up if name is taken', async () => { - const document = await parseDocument( - cmServices, - `diagram { - node nodeA for A { x := 10; y := 10; width := 10; height := 10; }; - node nodeA1 for A { x := 10; y := 10; width := 10; height := 10; }; - }` - ); - assert.strictEqual(findAvailableNodeName(document.parseResult.value.diagram!, 'nodeA'), 'nodeA2'); - }); - - test('should find lowest count if multiple are taken', async () => { - const document = await parseDocument( - cmServices, - `diagram { - node nodeA for A { x := 10; y := 10; width := 10; height := 10; }; - node nodeA1 for A { x := 10; y := 10; width := 10; height := 10; }; - node nodeA2 for A { x := 10; y := 10; width := 10; height := 10; }; - node nodeA4 for A { x := 10; y := 10; width := 10; height := 10; }; - }` - ); - assert.strictEqual(findAvailableNodeName(document.parseResult.value.diagram!, 'nodeA'), 'nodeA3'); - }); - }); -}); diff --git a/extensions/crossmodel-lang/test/language-server/lexer/cross-model-token-generator.test.ts b/extensions/crossmodel-lang/test/language-server/lexer/cross-model-token-generator.test.ts index fbb47de8..211f5ac5 100644 --- a/extensions/crossmodel-lang/test/language-server/lexer/cross-model-token-generator.test.ts +++ b/extensions/crossmodel-lang/test/language-server/lexer/cross-model-token-generator.test.ts @@ -5,45 +5,50 @@ import { describe, expect, test, beforeAll } from '@jest/globals'; import { Grammar } from 'langium'; -import { ExampleGrammarWithIndent } from '../test-utils/example-grammar'; import { TokenType } from 'chevrotain'; -import { ExampleGrammarWithNoIndent } from '../test-utils/example-grammar-no-indent'; import { DEDENT, INDENT, NEWLINE, SPACES } from '../../../src/language-server/lexer/cross-model-indentation-tokens'; import { CrossModelTokenBuilder } from '../../../src/language-server/lexer/cross-model-token-generator'; +import { CrossModelGrammar } from '../../../src/language-server/generated/grammar'; +import _ from 'lodash'; describe('CrossModelTokenBuilder', () => { let tokenBuilder: CrossModelTokenBuilder; - let exampleGrammerWithIndentation: Grammar; - let exampleGrammerWithNoIndentation: Grammar; + let crossModelGrammer: Grammar; + let crossModelGrammerWithoutIndentation: Grammar; beforeAll(() => { tokenBuilder = new CrossModelTokenBuilder(); - exampleGrammerWithIndentation = ExampleGrammarWithIndent(); - exampleGrammerWithNoIndentation = ExampleGrammarWithNoIndent(); + crossModelGrammer = CrossModelGrammar(); + // CrossModelGrammar loads the grammar in memory instead of making a new grammar + crossModelGrammerWithoutIndentation = _.cloneDeep(crossModelGrammer); + + crossModelGrammerWithoutIndentation.rules = crossModelGrammerWithoutIndentation.rules.filter( + rule => ![DEDENT.name, INDENT.name, NEWLINE.name, SPACES.name].includes(rule.name) + ); }); describe('buildTokens', () => { test('Should give NEWLINE token in first spot', () => { - const tokens = tokenBuilder.buildTokens(exampleGrammerWithIndentation) as TokenType[]; + const tokens = tokenBuilder.buildTokens(crossModelGrammer) as TokenType[]; expect(tokens[0]).toBe(NEWLINE); }); test('Should give DEDENT token in second spot', () => { - const tokens = tokenBuilder.buildTokens(exampleGrammerWithIndentation) as TokenType[]; + const tokens = tokenBuilder.buildTokens(crossModelGrammer) as TokenType[]; expect(tokens[1]).toBe(DEDENT); }); test('Should give INDENT token in third spot', () => { - const tokens = tokenBuilder.buildTokens(exampleGrammerWithIndentation) as TokenType[]; + const tokens = tokenBuilder.buildTokens(crossModelGrammer) as TokenType[]; expect(tokens[2]).toBe(INDENT); }); test('Should give SPACE token in last spot', () => { - const tokens = tokenBuilder.buildTokens(exampleGrammerWithIndentation) as TokenType[]; + const tokens = tokenBuilder.buildTokens(crossModelGrammer) as TokenType[]; const spaceToken = tokens.pop(); expect(spaceToken).toBe(SPACES); @@ -51,8 +56,8 @@ describe('CrossModelTokenBuilder', () => { test('Should throw error when missing indentation in grammar', () => { expect(() => { - tokenBuilder.buildTokens(exampleGrammerWithNoIndentation) as TokenType[]; - }).toThrow(); + tokenBuilder.buildTokens(crossModelGrammerWithoutIndentation) as TokenType[]; + }).toThrow(new Error('Missing indentation, new line or spaces tokens in grammar')); }); }); }); diff --git a/extensions/crossmodel-lang/test/language-server/test-utils/example-grammar-no-indent.ts b/extensions/crossmodel-lang/test/language-server/test-utils/example-grammar-no-indent.ts deleted file mode 100644 index ff372c56..00000000 --- a/extensions/crossmodel-lang/test/language-server/test-utils/example-grammar-no-indent.ts +++ /dev/null @@ -1,1343 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2023 CrossBreeze. - ********************************************************************************/ - -import { loadGrammarFromJson, Grammar } from 'langium'; - -let loadedExampleGrammarWithIndent: Grammar | undefined; -export const ExampleGrammarWithNoIndent = (): Grammar => - loadedExampleGrammarWithIndent ?? - (loadedExampleGrammarWithIndent = loadGrammarFromJson(`{ - "$type": "Grammar", - "isDeclared": true, - "name": "CrossModel", - "rules": [ - { - "$type": "ParserRule", - "name": "CrossModelRoot", - "entry": true, - "definition": { - "$type": "Alternatives", - "elements": [ - { - "$type": "Assignment", - "feature": "entity", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@1" - }, - "arguments": [] - } - }, - { - "$type": "Assignment", - "feature": "relationship", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@6" - }, - "arguments": [] - } - }, - { - "$type": "Assignment", - "feature": "diagram", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@9" - }, - "arguments": [] - } - } - ], - "cardinality": "?" - }, - "definesHiddenTokens": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "Entity", - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "entity" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Group", - "elements": [ - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@23" - }, - "arguments": [] - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@2" - }, - "arguments": [], - "cardinality": "*" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@22" - }, - "arguments": [] - } - ], - "cardinality": "*" - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "EntityFields", - "inferredType": { - "$type": "InferredType", - "name": "Entity" - }, - "definition": { - "$type": "Alternatives", - "elements": [ - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "id" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "name", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "name" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "name_val", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "description" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "description", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "attributes" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@3" - }, - "arguments": [] - } - ] - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "EntityAttributes", - "inferredType": { - "$type": "InferredType", - "name": "EntityFields" - }, - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@23" - }, - "arguments": [] - }, - { - "$type": "Assignment", - "feature": "attributes", - "operator": "+=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@4" - }, - "arguments": [] - }, - "cardinality": "*" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@22" - }, - "arguments": [] - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "EntityAttribute", - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "-" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@5" - }, - "arguments": [], - "cardinality": "*" - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "EntityAttributeFields", - "inferredType": { - "$type": "InferredType", - "name": "EntityAttribute" - }, - "definition": { - "$type": "Alternatives", - "elements": [ - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "id" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "name", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "name" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "name_val", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "datatype" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "datatype", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "description" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "description", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - } - } - ] - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "Relationship", - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "relationship" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Group", - "elements": [ - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@23" - }, - "arguments": [] - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@7" - }, - "arguments": [], - "cardinality": "*" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@22" - }, - "arguments": [] - } - ], - "cardinality": "*" - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "RelationshipFields", - "inferredType": { - "$type": "InferredType", - "name": "Relationship" - }, - "definition": { - "$type": "Alternatives", - "elements": [ - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "id" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "name", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "name" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "name_val", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "description" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "description", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "parent" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "parent", - "operator": "=", - "terminal": { - "$type": "CrossReference", - "type": { - "$ref": "#/rules@1" - }, - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@17" - }, - "arguments": [] - }, - "deprecatedSyntax": false - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "child" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "child", - "operator": "=", - "terminal": { - "$type": "CrossReference", - "type": { - "$ref": "#/rules@1" - }, - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@17" - }, - "arguments": [] - }, - "deprecatedSyntax": false - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "type" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "type", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@8" - }, - "arguments": [] - } - } - ] - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "RelationshipType", - "dataType": "string", - "definition": { - "$type": "Alternatives", - "elements": [ - { - "$type": "Keyword", - "value": "1:1" - }, - { - "$type": "Keyword", - "value": "1:n" - }, - { - "$type": "Keyword", - "value": "n:1" - }, - { - "$type": "Keyword", - "value": "n:m" - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "SystemDiagram", - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "diagram" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Group", - "elements": [ - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@23" - }, - "arguments": [] - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@10" - }, - "arguments": [], - "cardinality": "*" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@22" - }, - "arguments": [] - } - ], - "cardinality": "*" - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "SystemDiagramFields", - "inferredType": { - "$type": "InferredType", - "name": "SystemDiagram" - }, - "definition": { - "$type": "Alternatives", - "elements": [ - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "nodes" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@11" - }, - "arguments": [] - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "edges" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@14" - }, - "arguments": [] - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "id" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "name", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - } - } - ] - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "SystemDiagramNodes", - "inferredType": { - "$type": "InferredType", - "name": "SystemDiagramFields" - }, - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@23" - }, - "arguments": [] - }, - { - "$type": "Assignment", - "feature": "nodes", - "operator": "+=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@12" - }, - "arguments": [] - }, - "cardinality": "*" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@22" - }, - "arguments": [] - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "DiagramNode", - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "-" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@13" - }, - "arguments": [], - "cardinality": "*" - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "DiagramNodeFields", - "inferredType": { - "$type": "InferredType", - "name": "DiagramNode" - }, - "definition": { - "$type": "Alternatives", - "elements": [ - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "id" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "name", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "for" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "for", - "operator": "=", - "terminal": { - "$type": "CrossReference", - "type": { - "$ref": "#/rules@1" - }, - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@17" - }, - "arguments": [] - }, - "deprecatedSyntax": false - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "x" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "x", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@19" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "y" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "y", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@19" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "width" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "width", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@19" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "height" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "height", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@19" - }, - "arguments": [] - } - } - ] - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "SystemDiagramEdge", - "inferredType": { - "$type": "InferredType", - "name": "SystemDiagramFields" - }, - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@23" - }, - "arguments": [] - }, - { - "$type": "Assignment", - "feature": "edges", - "operator": "+=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@15" - }, - "arguments": [] - }, - "cardinality": "*" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@22" - }, - "arguments": [] - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "DiagramEdge", - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "-" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@16" - }, - "arguments": [], - "cardinality": "*" - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "DiagramEdgeFields", - "inferredType": { - "$type": "InferredType", - "name": "DiagramEdge" - }, - "definition": { - "$type": "Alternatives", - "elements": [ - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "for" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "for", - "operator": "=", - "terminal": { - "$type": "CrossReference", - "type": { - "$ref": "#/rules@6" - }, - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@17" - }, - "arguments": [] - }, - "deprecatedSyntax": false - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "id" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "name", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - } - } - ] - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "QualifiedName", - "dataType": "string", - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "." - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - } - ], - "cardinality": "*" - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "TerminalRule", - "name": "STRING", - "definition": { - "$type": "RegexToken", - "regex": "\\"[^\\"]*\\"|'[^']*'" - }, - "fragment": false, - "hidden": false - }, - { - "$type": "TerminalRule", - "name": "NUMBER", - "type": { - "$type": "ReturnType", - "name": "number" - }, - "definition": { - "$type": "RegexToken", - "regex": "(-)?[0-9]+(\\\\.[0-9]*)?" - }, - "fragment": false, - "hidden": false - }, - { - "$type": "TerminalRule", - "hidden": true, - "name": "SL_COMMENT", - "definition": { - "$type": "RegexToken", - "regex": "#[^\\\\n\\\\r]*" - }, - "fragment": false - }, - { - "$type": "TerminalRule", - "hidden": true, - "name": "NEWLINE", - "definition": { - "$type": "CharacterRange", - "left": { - "$type": "Keyword", - "value": "this_string_does_not_matter_newline#$%^&*((" - } - }, - "fragment": false - }, - { - "$type": "TerminalRule", - "name": "DEDENT", - "definition": { - "$type": "CharacterRange", - "left": { - "$type": "Keyword", - "value": "this_string_does_not_matter_dedent#$%^&*((" - } - }, - "fragment": false, - "hidden": false - }, - { - "$type": "TerminalRule", - "hidden": true, - "name": "SPACES", - "definition": { - "$type": "CharacterRange", - "left": { - "$type": "Keyword", - "value": "this_string_does_not_matter_spaces#$%^&*((" - } - }, - "fragment": false - } - ], - "definesHiddenTokens": false, - "hiddenTokens": [], - "imports": [], - "interfaces": [], - "types": [], - "usedGrammars": [] -}`)); diff --git a/extensions/crossmodel-lang/test/language-server/test-utils/example-grammar.ts b/extensions/crossmodel-lang/test/language-server/test-utils/example-grammar.ts deleted file mode 100644 index ab5f75a7..00000000 --- a/extensions/crossmodel-lang/test/language-server/test-utils/example-grammar.ts +++ /dev/null @@ -1,1356 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2023 CrossBreeze. - ********************************************************************************/ - -import { loadGrammarFromJson, Grammar } from 'langium'; - -let loadedExampleGrammarWithIndent: Grammar | undefined; -export const ExampleGrammarWithIndent = (): Grammar => - loadedExampleGrammarWithIndent ?? - (loadedExampleGrammarWithIndent = loadGrammarFromJson(`{ - "$type": "Grammar", - "isDeclared": true, - "name": "CrossModel", - "rules": [ - { - "$type": "ParserRule", - "name": "CrossModelRoot", - "entry": true, - "definition": { - "$type": "Alternatives", - "elements": [ - { - "$type": "Assignment", - "feature": "entity", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@1" - }, - "arguments": [] - } - }, - { - "$type": "Assignment", - "feature": "relationship", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@6" - }, - "arguments": [] - } - }, - { - "$type": "Assignment", - "feature": "diagram", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@9" - }, - "arguments": [] - } - } - ], - "cardinality": "?" - }, - "definesHiddenTokens": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "Entity", - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "entity" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Group", - "elements": [ - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@23" - }, - "arguments": [] - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@2" - }, - "arguments": [], - "cardinality": "*" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@22" - }, - "arguments": [] - } - ], - "cardinality": "*" - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "EntityFields", - "inferredType": { - "$type": "InferredType", - "name": "Entity" - }, - "definition": { - "$type": "Alternatives", - "elements": [ - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "id" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "name", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "name" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "name_val", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "description" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "description", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "attributes" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@3" - }, - "arguments": [] - } - ] - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "EntityAttributes", - "inferredType": { - "$type": "InferredType", - "name": "EntityFields" - }, - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@23" - }, - "arguments": [] - }, - { - "$type": "Assignment", - "feature": "attributes", - "operator": "+=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@4" - }, - "arguments": [] - }, - "cardinality": "*" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@22" - }, - "arguments": [] - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "EntityAttribute", - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "-" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@5" - }, - "arguments": [], - "cardinality": "*" - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "EntityAttributeFields", - "inferredType": { - "$type": "InferredType", - "name": "EntityAttribute" - }, - "definition": { - "$type": "Alternatives", - "elements": [ - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "id" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "name", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "name" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "name_val", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "datatype" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "datatype", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "description" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "description", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - } - } - ] - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "Relationship", - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "relationship" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Group", - "elements": [ - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@23" - }, - "arguments": [] - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@7" - }, - "arguments": [], - "cardinality": "*" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@22" - }, - "arguments": [] - } - ], - "cardinality": "*" - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "RelationshipFields", - "inferredType": { - "$type": "InferredType", - "name": "Relationship" - }, - "definition": { - "$type": "Alternatives", - "elements": [ - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "id" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "name", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "name" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "name_val", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "description" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "description", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "parent" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "parent", - "operator": "=", - "terminal": { - "$type": "CrossReference", - "type": { - "$ref": "#/rules@1" - }, - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@17" - }, - "arguments": [] - }, - "deprecatedSyntax": false - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "child" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "child", - "operator": "=", - "terminal": { - "$type": "CrossReference", - "type": { - "$ref": "#/rules@1" - }, - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@17" - }, - "arguments": [] - }, - "deprecatedSyntax": false - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "type" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "type", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@8" - }, - "arguments": [] - } - } - ] - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "RelationshipType", - "dataType": "string", - "definition": { - "$type": "Alternatives", - "elements": [ - { - "$type": "Keyword", - "value": "1:1" - }, - { - "$type": "Keyword", - "value": "1:n" - }, - { - "$type": "Keyword", - "value": "n:1" - }, - { - "$type": "Keyword", - "value": "n:m" - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "SystemDiagram", - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "diagram" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Group", - "elements": [ - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@23" - }, - "arguments": [] - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@10" - }, - "arguments": [], - "cardinality": "*" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@22" - }, - "arguments": [] - } - ], - "cardinality": "*" - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "SystemDiagramFields", - "inferredType": { - "$type": "InferredType", - "name": "SystemDiagram" - }, - "definition": { - "$type": "Alternatives", - "elements": [ - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "nodes" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@11" - }, - "arguments": [] - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "edges" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@14" - }, - "arguments": [] - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "id" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "name", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - } - } - ] - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "SystemDiagramNodes", - "inferredType": { - "$type": "InferredType", - "name": "SystemDiagramFields" - }, - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@23" - }, - "arguments": [] - }, - { - "$type": "Assignment", - "feature": "nodes", - "operator": "+=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@12" - }, - "arguments": [] - }, - "cardinality": "*" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@22" - }, - "arguments": [] - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "DiagramNode", - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "-" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@13" - }, - "arguments": [], - "cardinality": "*" - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "DiagramNodeFields", - "inferredType": { - "$type": "InferredType", - "name": "DiagramNode" - }, - "definition": { - "$type": "Alternatives", - "elements": [ - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "id" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "name", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "for" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "for", - "operator": "=", - "terminal": { - "$type": "CrossReference", - "type": { - "$ref": "#/rules@1" - }, - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@17" - }, - "arguments": [] - }, - "deprecatedSyntax": false - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "x" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "x", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@19" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "y" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "y", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@19" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "width" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "width", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@19" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "height" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "height", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@19" - }, - "arguments": [] - } - } - ] - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "SystemDiagramEdge", - "inferredType": { - "$type": "InferredType", - "name": "SystemDiagramFields" - }, - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@23" - }, - "arguments": [] - }, - { - "$type": "Assignment", - "feature": "edges", - "operator": "+=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@15" - }, - "arguments": [] - }, - "cardinality": "*" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@22" - }, - "arguments": [] - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "DiagramEdge", - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "-" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@16" - }, - "arguments": [], - "cardinality": "*" - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "DiagramEdgeFields", - "inferredType": { - "$type": "InferredType", - "name": "DiagramEdge" - }, - "definition": { - "$type": "Alternatives", - "elements": [ - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "for" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "for", - "operator": "=", - "terminal": { - "$type": "CrossReference", - "type": { - "$ref": "#/rules@6" - }, - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@17" - }, - "arguments": [] - }, - "deprecatedSyntax": false - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "id" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "name", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - } - } - ] - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "QualifiedName", - "dataType": "string", - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "." - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" - }, - "arguments": [] - } - ], - "cardinality": "*" - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "TerminalRule", - "name": "STRING", - "definition": { - "$type": "RegexToken", - "regex": "\\"[^\\"]*\\"|'[^']*'" - }, - "fragment": false, - "hidden": false - }, - { - "$type": "TerminalRule", - "name": "NUMBER", - "type": { - "$type": "ReturnType", - "name": "number" - }, - "definition": { - "$type": "RegexToken", - "regex": "(-)?[0-9]+(\\\\.[0-9]*)?" - }, - "fragment": false, - "hidden": false - }, - { - "$type": "TerminalRule", - "hidden": true, - "name": "SL_COMMENT", - "definition": { - "$type": "RegexToken", - "regex": "#[^\\\\n\\\\r]*" - }, - "fragment": false - }, - { - "$type": "TerminalRule", - "hidden": true, - "name": "NEWLINE", - "definition": { - "$type": "CharacterRange", - "left": { - "$type": "Keyword", - "value": "this_string_does_not_matter_newline#$%^&*((" - } - }, - "fragment": false - }, - { - "$type": "TerminalRule", - "name": "DEDENT", - "definition": { - "$type": "CharacterRange", - "left": { - "$type": "Keyword", - "value": "this_string_does_not_matter_dedent#$%^&*((" - } - }, - "fragment": false, - "hidden": false - }, - { - "$type": "TerminalRule", - "name": "INDENT", - "definition": { - "$type": "CharacterRange", - "left": { - "$type": "Keyword", - "value": "this_string_does_not_matter_indent#$%^&*((" - } - }, - "fragment": false, - "hidden": false - }, - { - "$type": "TerminalRule", - "hidden": true, - "name": "SPACES", - "definition": { - "$type": "CharacterRange", - "left": { - "$type": "Keyword", - "value": "this_string_does_not_matter_spaces#$%^&*((" - } - }, - "fragment": false - } - ], - "definesHiddenTokens": false, - "hiddenTokens": [], - "imports": [], - "interfaces": [], - "types": [], - "usedGrammars": [] -}`)); diff --git a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/relationship/relationship1.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/relationship/relationship1.ts index 569bfb9d..7eda160e 100644 --- a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/relationship/relationship1.ts +++ b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/relationship/relationship1.ts @@ -5,6 +5,6 @@ export const relationship1 = `relationship: id: 'Order_Customer' parent: 'Customer' child: 'Order' - type: 1:1 + type: "1:1" name: "Customer Order relationship" description: "A relationship between a customer and an order."`; diff --git a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/relationship/relationship2.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/relationship/relationship2.ts index df39b1c0..53d57d99 100644 --- a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/relationship/relationship2.ts +++ b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/relationship/relationship2.ts @@ -5,4 +5,4 @@ export const relationship2 = `relationship: id: 'Order_Customer' parent: 'Customer' child: 'Order' - type: 1:1`; + type: "1:1"`; diff --git a/extensions/crossmodel-lang/test/language-server/util/name-util.test.ts b/extensions/crossmodel-lang/test/language-server/util/name-util.test.ts new file mode 100644 index 00000000..c3acb466 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/util/name-util.test.ts @@ -0,0 +1,58 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +import { describe, expect, test } from '@jest/globals'; +import { EmptyFileSystem } from 'langium'; + +import { createCrossModelServices } from '../../../src/language-server/cross-model-module'; +import { CrossModelRoot } from '../../../src/language-server/generated/ast'; +import { findAvailableNodeName } from '../../../src/language-server/util/name-util'; +import { parseDocument } from '../test-utils/utils'; + +const services = createCrossModelServices({ ...EmptyFileSystem }); +const cmServices = services.CrossModel; + +const ex1 = 'diagram:'; +const ex2 = `diagram: + nodes: + - id: "nodeA"`; +const ex3 = `diagram: + nodes: + - id: "nodeA" + - id: "nodeA1"`; +const ex4 = `diagram: + nodes: + - id: "nodeA" + - id: "nodeA1" + - id: "nodeA2" + - id: "nodeA4"`; + +describe('NameUtil', () => { + describe('findAvailableNodeName', () => { + test('should return given name if unique', async () => { + const document = await parseDocument(cmServices, ex1); + + expect(findAvailableNodeName(document.parseResult.value.diagram!, 'nodeA')).toBe('nodeA'); + }); + + test('should return unique name if given is taken', async () => { + const document = await parseDocument(cmServices, ex2); + + const resultat = findAvailableNodeName(document.parseResult.value.diagram!, 'nodeA'); + + expect(resultat).toBe('nodeA1'); + }); + + test('should properly count up if name is taken', async () => { + const document = await parseDocument(cmServices, ex3); + + expect(findAvailableNodeName(document.parseResult.value.diagram!, 'nodeA')).toBe('nodeA2'); + }); + + test('should find lowest count if multiple are taken', async () => { + const document = await parseDocument(cmServices, ex4); + + expect(findAvailableNodeName(document.parseResult.value.diagram!, 'nodeA')).toBe('nodeA3'); + }); + }); +}); diff --git a/package.json b/package.json index ea5f0f2d..a7f539ee 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "start:browser": "yarn theia:browser start", "start:electron": "yarn theia:electron start", "start:verdaccio": "yarn verdaccio --config verdaccio-config.yaml", + "test": "yarn --cwd extensions/crossmodel-lang test", "theia:browser": "yarn --cwd applications/browser-app", "theia:electron": "yarn --cwd applications/electron-app", "watch": "lerna run --parallel watch" From f895231b49bea7c88af3443eb88c5553aaab5d74 Mon Sep 17 00:00:00 2001 From: Martin Fleck Date: Wed, 27 Sep 2023 15:08:15 +0200 Subject: [PATCH 13/22] Improve stability of cross-updates and fix edge structure - Ensure edges (relationships) are created between nodes (entities) -- Ensure node entity matches expected relationship entity - Improve stability of cross-updates -- Ensure 'save' does an implicit 'update' of the internal structure -- Ensure update only updates text editor when it was opened -- Only do full textual updates to avoid merging issues -- Let UI react to updates, not only on save -- Debounce model update for form editor - Minors -- Adapt grammar to better reflect semantic element (instead of 'for') -- Always serialize properties in same order --- .vscode/launch.json | 28 ++++---- .../voorbeeld_taal_entity_customer.cm | 30 ++++---- .../entities/voorbeeld_taal_entity_order.cm | 24 +++---- ...oorbeeld_taal_relationship.relationship.cm | 2 +- .../source/voorbeeld_taal_diagram.diagram.cm | 18 ++--- .../handler/add-entity-operation-handler.ts | 2 +- .../handler/create-edge-operation-handler.ts | 12 ++-- .../handler/drop-entity-operation-handler.ts | 2 +- .../model/builders/node-builder.ts | 2 +- .../model/cross-model-gmodel-factory.ts | 13 ++-- .../cross-model-language-server.ts | 13 ++++ .../src/language-server/cross-model-module.ts | 6 +- .../language-server/cross-model-serializer.ts | 23 ++++++ .../language-server/cross-model-validator.ts | 14 +++- .../src/language-server/cross-model.langium | 6 +- .../src/language-server/generated/ast.ts | 14 ++-- .../src/language-server/generated/grammar.ts | 72 +++++++++++++++++-- .../src/model-server/model-server.ts | 13 ++-- .../src/model-server/model-service.ts | 31 ++++++-- .../open-text-document-manager.ts | 12 ++-- .../model-server/openable-text-documents.ts | 25 ++++++- .../syntaxes/cross-model.tmLanguage.json | 2 +- .../cross-model-lang-diagram.test.ts | 28 ++++---- .../serializer/cross-model-serializer.test.ts | 47 ++++++------ .../test-documents/diagram/diagram2.ts | 2 +- .../test-documents/diagram/diagram3.ts | 2 +- .../test-documents/diagram/diagram5.ts | 4 +- .../test-documents/diagram/diagram6.ts | 4 +- .../src/browser/form-editor-widget.tsx | 43 ++++++----- .../src/browser/react-components/App.tsx | 33 ++++----- .../browser/react-components/ModelContext.tsx | 19 +++-- .../model-service/src/node/model-service.ts | 8 ++- .../protocol/src/model-service/protocol.ts | 1 + 33 files changed, 369 insertions(+), 186 deletions(-) create mode 100644 extensions/crossmodel-lang/src/language-server/cross-model-language-server.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index b8b55b41..4b660e1f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -22,7 +22,7 @@ "--loglevel=debug", "--hostname=localhost", "--no-cluster", - "--root-dir=${workspaceRoot}/examples/workspace", + "--root-dir=${workspaceRoot}/examples/yaml-example", "--app-project-path=${workspaceFolder}/applications/electron-app", "--remote-debugging-port=9222", "--no-app-auto-install", @@ -116,18 +116,18 @@ ] }, { - "name": "Debug Jest Tests", - "type": "node", - "request": "launch", - "runtimeArgs": [ - "--inspect-brk", - "${workspaceRoot}/node_modules/jest/bin/jest.js", - "--runInBand", - "--config=extensions/crossmodel-lang/jest.config.js", - "--testPathPattern=extensions/crossmodel-lang/test/language-server/util/name-util.test.ts" - ], - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen" - } + "name": "Debug Jest Tests", + "type": "node", + "request": "launch", + "runtimeArgs": [ + "--inspect-brk", + "${workspaceRoot}/node_modules/jest/bin/jest.js", + "--runInBand", + "--config=extensions/crossmodel-lang/jest.config.js", + "--testPathPattern=extensions/crossmodel-lang/test/language-server/util/name-util.test.ts" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + } ] } diff --git a/examples/yaml-example/entities/voorbeeld_taal_entity_customer.cm b/examples/yaml-example/entities/voorbeeld_taal_entity_customer.cm index d3174888..ac159308 100644 --- a/examples/yaml-example/entities/voorbeeld_taal_entity_customer.cm +++ b/examples/yaml-example/entities/voorbeeld_taal_entity_customer.cm @@ -1,23 +1,23 @@ entity: + name: "Customer" id: "Customer" + description: "A customer with whom a transaction has been made." attributes: - - id: "Id" - name: "Id" + - name: "Id" + id: "Id" datatype: "Integer" - - id: "FirstName" - name: "FirstName" - datatype: "Varchar" - - id: "LastName" - name: "LastName" + - name: "FirstName" + id: "FirstName" datatype: "Varchar" - - id: "City" - name: "City" + - name: "LastName" + id: "LastName" datatype: "Varchar" - - id: "Country" - name: "Country" + - name: "City" + id: "City" datatype: "Varchar" - - id: "Phone" - name: "Phone" + - name: "Country" + id: "Country" datatype: "Varchar" - name: "Customer" - description: "A customer with whom a transaction has been made." \ No newline at end of file + - name: "Phone" + id: "Phone" + datatype: "Varchar" \ No newline at end of file diff --git a/examples/yaml-example/entities/voorbeeld_taal_entity_order.cm b/examples/yaml-example/entities/voorbeeld_taal_entity_order.cm index 13716962..1f989673 100644 --- a/examples/yaml-example/entities/voorbeeld_taal_entity_order.cm +++ b/examples/yaml-example/entities/voorbeeld_taal_entity_order.cm @@ -1,19 +1,19 @@ entity: id: "Order" + description: "Order placed by a customer in the Customer table." attributes: - - id: "Id" - name: "Id" + - name: "Id" + id: "Id" datatype: "Integer" - - id: "OrderDate" - name: "OrderDate" + - name: "OrderDate" + id: "OrderDate" datatype: "Integer" - - id: "OrderNumber" - name: "OrderNumber" + - name: "OrderNumber" + id: "OrderNumber" datatype: "Varchar" - - id: "CustomerId" - name: "CustomerId" + - name: "CustomerId" + id: "CustomerId" datatype: "Integer" - - id: "TotalAmount" - name: "TotalAmount" - datatype: "Float" - description: "Order placed by a customer in the Customer table." \ No newline at end of file + - name: "TotalAmount" + id: "TotalAmount" + datatype: "Float" \ No newline at end of file diff --git a/examples/yaml-example/relationships/voorbeeld_taal_relationship.relationship.cm b/examples/yaml-example/relationships/voorbeeld_taal_relationship.relationship.cm index 2e5ef342..fdfd4ab1 100644 --- a/examples/yaml-example/relationships/voorbeeld_taal_relationship.relationship.cm +++ b/examples/yaml-example/relationships/voorbeeld_taal_relationship.relationship.cm @@ -2,4 +2,4 @@ relationship: id: 'Order_Customer' parent: 'Customer' child: 'Order' - type: "1:1" \ No newline at end of file + type: '1:1' \ No newline at end of file diff --git a/examples/yaml-example/source/voorbeeld_taal_diagram.diagram.cm b/examples/yaml-example/source/voorbeeld_taal_diagram.diagram.cm index 4b52552d..420cb591 100644 --- a/examples/yaml-example/source/voorbeeld_taal_diagram.diagram.cm +++ b/examples/yaml-example/source/voorbeeld_taal_diagram.diagram.cm @@ -1,19 +1,21 @@ diagram: id: "Systemdiagram1" - edges: - - id: "OrderCustomerEdge" - for: "Order_Customer" + description: "This should be the description" nodes: - id: "CustomerNode" - for: "Customer" + entity: "Customer" x: 322.5893891316664 y: 262.8195919791379 - height: 151 width: 122.22364807128906 + height: 151 - id: "OrderNode" - for: "Order" + entity: "Order" x: 655.4416344093675 y: 270.85224527770106 - height: 132 width: 139.6079559326172 - description: "This should be the description" \ No newline at end of file + height: 132 + edges: + - id: "CustomerToOrder" + relationship: "Order_Customer" + sourceNode: "CustomerNode" + targetNode: "OrderNode" \ No newline at end of file diff --git a/extensions/crossmodel-lang/src/glsp-server/handler/add-entity-operation-handler.ts b/extensions/crossmodel-lang/src/glsp-server/handler/add-entity-operation-handler.ts index 0befa0d3..b3a15bf2 100644 --- a/extensions/crossmodel-lang/src/glsp-server/handler/add-entity-operation-handler.ts +++ b/extensions/crossmodel-lang/src/glsp-server/handler/add-entity-operation-handler.ts @@ -36,7 +36,7 @@ export class CrossModelAddEntityOperationHandler extends OperationHandler { $type: DiagramNode, $container: container, name: findAvailableNodeName(container, entityDescription.name + 'Node'), - for: { + entity: { $refText: entityDescription.name, ref: entityDescription.node as Entity | undefined }, diff --git a/extensions/crossmodel-lang/src/glsp-server/handler/create-edge-operation-handler.ts b/extensions/crossmodel-lang/src/glsp-server/handler/create-edge-operation-handler.ts index 4cb98914..636f55f9 100644 --- a/extensions/crossmodel-lang/src/glsp-server/handler/create-edge-operation-handler.ts +++ b/extensions/crossmodel-lang/src/glsp-server/handler/create-edge-operation-handler.ts @@ -48,7 +48,9 @@ export class CrossModelCreateEdgeOperationHandler extends OperationHandler imple $type: DiagramEdge, $container: this.state.diagramRoot, name: relationship.name, - for: { ref: relationship, $refText: this.state.nameProvider.getName(relationship) || relationship.name || '' } + relationship: { ref: relationship, $refText: this.state.nameProvider.getName(relationship) || relationship.name || '' }, + sourceNode: { ref: sourceNode, $refText: this.state.nameProvider.getLocalName(sourceNode) || sourceNode.name || '' }, + targetNode: { ref: targetNode, $refText: this.state.nameProvider.getLocalName(targetNode) || targetNode.name || '' } }; this.state.diagramRoot.edges.push(edge); } @@ -59,8 +61,8 @@ export class CrossModelCreateEdgeOperationHandler extends OperationHandler imple * Creates a new relationship and stores it on a file on the file system. */ protected async createAndSaveRelationship(sourceNode: DiagramNode, targetNode: DiagramNode): Promise { - const source = sourceNode.for?.ref?.name || sourceNode.for?.$refText; - const target = targetNode.for?.ref?.name || targetNode.for?.$refText; + const source = sourceNode.entity?.ref?.name || sourceNode.entity?.$refText; + const target = targetNode.entity?.ref?.name || targetNode.entity?.$refText; // search for unique file name for the relationship and use file base name as relationship name // if the user doesn't rename any files we should end up with unique names ;-) @@ -76,8 +78,8 @@ export class CrossModelCreateEdgeOperationHandler extends OperationHandler imple $container: relationshipRoot, name, type: '1:1', - parent: { $refText: sourceNode.for?.$refText || '' }, - child: { $refText: targetNode.for?.$refText || '' } + parent: { $refText: sourceNode.entity?.$refText || '' }, + child: { $refText: targetNode.entity?.$refText || '' } }; relationshipRoot.relationship = relationship; const text = this.state.semanticSerializer.serialize(relationshipRoot); diff --git a/extensions/crossmodel-lang/src/glsp-server/handler/drop-entity-operation-handler.ts b/extensions/crossmodel-lang/src/glsp-server/handler/drop-entity-operation-handler.ts index 609ccb93..e1f10b0e 100644 --- a/extensions/crossmodel-lang/src/glsp-server/handler/drop-entity-operation-handler.ts +++ b/extensions/crossmodel-lang/src/glsp-server/handler/drop-entity-operation-handler.ts @@ -38,7 +38,7 @@ export class CrossModelDropEntityOperationHandler extends OperationHandler { $type: DiagramNode, $container: container, name: findAvailableNodeName(container, root.entity.name + 'Node'), - for: { + entity: { $refText: this.state.nameProvider.getFullyQualifiedName(root.entity) || root.entity.name || '', ref: root.entity }, diff --git a/extensions/crossmodel-lang/src/glsp-server/model/builders/node-builder.ts b/extensions/crossmodel-lang/src/glsp-server/model/builders/node-builder.ts index f5a23035..504636ac 100644 --- a/extensions/crossmodel-lang/src/glsp-server/model/builders/node-builder.ts +++ b/extensions/crossmodel-lang/src/glsp-server/model/builders/node-builder.ts @@ -15,7 +15,7 @@ export class GEntityNode extends GNode { export class GEntityNodeBuilder extends GNodeBuilder { addNode(node: DiagramNode): this { // Get the reference that the DiagramNode holds to the Entity in the .langium file. - const entityRef = node.for?.ref; + const entityRef = node.entity?.ref; // Options which are the same for every node this.addCssClasses('diagram-node', 'entity').layout('vbox').addArgs(ArgsUtil.cornerRadius(3)); diff --git a/extensions/crossmodel-lang/src/glsp-server/model/cross-model-gmodel-factory.ts b/extensions/crossmodel-lang/src/glsp-server/model/cross-model-gmodel-factory.ts index a077a5cf..490a32ca 100644 --- a/extensions/crossmodel-lang/src/glsp-server/model/cross-model-gmodel-factory.ts +++ b/extensions/crossmodel-lang/src/glsp-server/model/cross-model-gmodel-factory.ts @@ -4,8 +4,8 @@ import { GEdge, GGraph, GModelFactory, GNode } from '@eclipse-glsp/server'; import { inject, injectable } from 'inversify'; import { DiagramEdge, DiagramNode } from '../../language-server/generated/ast'; -import { CrossModelState } from './cross-model-state'; import { GEntityNode } from './builders/node-builder'; +import { CrossModelState } from './cross-model-state'; /** * Custom factory that translates the semantic diagram root from Langium to a GLSP graph. @@ -29,7 +29,7 @@ export class CrossModelGModelFactory implements GModelFactory { const graphBuilder = GGraph.builder().id(this.modelState.semanticUri); diagramRoot.nodes.map(node => this.createDiagramNode(node)).forEach(node => graphBuilder.add(node)); - diagramRoot.edges.map(edge => this.createDiagramEdge(edge, diagramRoot.nodes)).forEach(edge => graphBuilder.add(edge)); + diagramRoot.edges.map(edge => this.createDiagramEdge(edge)).forEach(edge => graphBuilder.add(edge)); return graphBuilder.build(); } @@ -41,14 +41,11 @@ export class CrossModelGModelFactory implements GModelFactory { return GEntityNode.builder().id(id).addNode(node).build(); } - protected createDiagramEdge(edge: DiagramEdge, diagramNodes: DiagramNode[]): GEdge { + protected createDiagramEdge(edge: DiagramEdge): GEdge { const id = this.modelState.index.createId(edge) ?? 'unknown'; - const parentRef = edge.for?.ref?.parent?.$refText; - const childRef = edge.for?.ref?.child?.$refText; - - const parentDiagramNode = diagramNodes.find(item => item.for?.ref?.name === parentRef)?.name; - const childDiagramNode = diagramNodes.find(item => item.for?.ref?.name === childRef)?.name; + const parentDiagramNode = edge.sourceNode?.ref?.name || edge.sourceNode?.$refText; + const childDiagramNode = edge.targetNode?.ref?.name || edge.targetNode?.$refText; return GEdge.builder() .id(id) diff --git a/extensions/crossmodel-lang/src/language-server/cross-model-language-server.ts b/extensions/crossmodel-lang/src/language-server/cross-model-language-server.ts new file mode 100644 index 00000000..91e7b266 --- /dev/null +++ b/extensions/crossmodel-lang/src/language-server/cross-model-language-server.ts @@ -0,0 +1,13 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +import { DefaultLanguageServer } from 'langium'; +import { TextDocumentSyncKind, type InitializeParams, type InitializeResult } from 'vscode-languageserver-protocol'; + +export class CrossModelLanguageServer extends DefaultLanguageServer { + override async initialize(params: InitializeParams): Promise { + const result = await super.initialize(params); + result.capabilities.textDocumentSync = TextDocumentSyncKind.Full; + return result; + } +} diff --git a/extensions/crossmodel-lang/src/language-server/cross-model-module.ts b/extensions/crossmodel-lang/src/language-server/cross-model-module.ts index faca6350..f1d8841b 100644 --- a/extensions/crossmodel-lang/src/language-server/cross-model-module.ts +++ b/extensions/crossmodel-lang/src/language-server/cross-model-module.ts @@ -28,6 +28,7 @@ import { CrossModelCompletionProvider } from './cross-model-completion-provider' import { CrossModelDocumentBuilder } from './cross-model-document-builder'; import { CrossModelModelFormatter } from './cross-model-formatter'; import { CrossModelLangiumDocuments } from './cross-model-langium-documents'; +import { CrossModelLanguageServer } from './cross-model-language-server'; import { QualifiedNameProvider } from './cross-model-naming'; import { CrossModelPackageManager } from './cross-model-package-manager'; import { CrossModelScopeComputation } from './cross-model-scope'; @@ -36,8 +37,8 @@ import { CrossModelSerializer } from './cross-model-serializer'; import { CrossModelValidator, registerValidationChecks } from './cross-model-validator'; import { CrossModelWorkspaceManager } from './cross-model-workspace-manager'; import { CrossModelGeneratedModule, CrossModelGeneratedSharedModule } from './generated/module'; -import { CrossModelTokenBuilder } from './lexer/cross-model-token-generator'; import { CrossModelLexer } from './lexer/cross-model-lexer'; +import { CrossModelTokenBuilder } from './lexer/cross-model-token-generator'; /*************************** * Shared Module @@ -101,6 +102,9 @@ export const CrossModelSharedModule: Module< logger: { ClientLogger: services => new ClientLogger(services) }, + lsp: { + LanguageServer: services => new CrossModelLanguageServer(services) + }, model: { ModelService: services => new ModelService(services) } diff --git a/extensions/crossmodel-lang/src/language-server/cross-model-serializer.ts b/extensions/crossmodel-lang/src/language-server/cross-model-serializer.ts index 197bc16a..642f7ce7 100644 --- a/extensions/crossmodel-lang/src/language-server/cross-model-serializer.ts +++ b/extensions/crossmodel-lang/src/language-server/cross-model-serializer.ts @@ -7,6 +7,28 @@ import { Serializer } from '../model-server/serializer'; import { CrossModelServices } from './cross-model-module'; import { CrossModelRoot, Entity, Relationship, SystemDiagram } from './generated/ast'; +const PROPERTY_ORDER = [ + 'id', + 'name', + 'name_val', + 'datatype', + 'description', + 'attributes', + 'parent', + 'child', + 'type', + 'nodes', + 'edges', + 'entity', + 'x', + 'y', + 'width', + 'height', + 'relationship', + 'sourceNode', + 'targetNode' +]; + /** * Hand-written AST serializer as there is currently no out-of-the box serializer from Langium, but it is on the roadmap. * cf. https://github.com/langium/langium/discussions/683 @@ -34,6 +56,7 @@ export class CrossModelSerializer implements Serializer { const indentation = ' '.repeat(indentationLevel); const serializedProperties = Object.entries(obj) + .sort((left, right) => PROPERTY_ORDER.indexOf(left[0]) - PROPERTY_ORDER.indexOf(right[0])) .map(([key, value]) => { if (Array.isArray(value) && value.length === 0) { return; diff --git a/extensions/crossmodel-lang/src/language-server/cross-model-validator.ts b/extensions/crossmodel-lang/src/language-server/cross-model-validator.ts index 54b79393..ae1b90ad 100644 --- a/extensions/crossmodel-lang/src/language-server/cross-model-validator.ts +++ b/extensions/crossmodel-lang/src/language-server/cross-model-validator.ts @@ -3,7 +3,7 @@ ********************************************************************************/ import { ValidationAcceptor, ValidationChecks } from 'langium'; import type { CrossModelServices } from './cross-model-module'; -import { CrossModelAstType, Entity, EntityAttribute, Relationship, SystemDiagram } from './generated/ast'; +import { CrossModelAstType, DiagramEdge, Entity, EntityAttribute, Relationship, SystemDiagram } from './generated/ast'; /** * Register custom validation checks. @@ -16,7 +16,8 @@ export function registerValidationChecks(services: CrossModelServices): void { Entity: validator.checkEntityHasNecessaryFields, EntityAttribute: validator.checkAttributeHasNecessaryFields, SystemDiagram: validator.checkSystemDiagramHasNecessaryFields, - Relationship: validator.checkRelationshipHasNecessaryFields + Relationship: validator.checkRelationshipHasNecessaryFields, + DiagramEdge: validator.checkDiagramEdge }; registry.register(checks, validator); } @@ -48,4 +49,13 @@ export class CrossModelValidator { accept('error', 'Attribute missing id field', { node: relationship, property: 'name' }); } } + + checkDiagramEdge(edge: DiagramEdge, accept: ValidationAcceptor): void { + if (edge.sourceNode?.ref?.entity?.ref?.$type !== edge.relationship?.ref?.parent?.ref?.$type) { + accept('error', 'Source must match type of parent', { node: edge, property: 'sourceNode' }); + } + if (edge.targetNode?.ref?.entity?.ref?.$type !== edge.relationship?.ref?.child?.ref?.$type) { + accept('error', 'Target must match type of child', { node: edge, property: 'targetNode' }); + } + } } diff --git a/extensions/crossmodel-lang/src/language-server/cross-model.langium b/extensions/crossmodel-lang/src/language-server/cross-model.langium index 8661c305..224bdb88 100644 --- a/extensions/crossmodel-lang/src/language-server/cross-model.langium +++ b/extensions/crossmodel-lang/src/language-server/cross-model.langium @@ -94,7 +94,7 @@ DiagramNode: DiagramNodeFields infers DiagramNode: 'id' ':' name=STRING | - 'for' ':' for=[Entity:QualifiedName] | + 'entity' ':' entity=[Entity:QualifiedName] | 'x' ':' x=NUMBER | 'y' ':' y=NUMBER | 'width' ':' width=NUMBER | @@ -115,7 +115,9 @@ DiagramEdge: DiagramEdgeFields infers DiagramEdge: ( - 'for' ':' for=[Relationship:QualifiedName] | + 'relationship' ':' relationship=[Relationship:QualifiedName] | + 'sourceNode' ':' sourceNode=[DiagramNode:QualifiedName] | + 'targetNode' ':' targetNode=[DiagramNode:QualifiedName] | 'id' ':' name=STRING ) ; diff --git a/extensions/crossmodel-lang/src/language-server/generated/ast.ts b/extensions/crossmodel-lang/src/language-server/generated/ast.ts index ceb5abe0..ed39a285 100644 --- a/extensions/crossmodel-lang/src/language-server/generated/ast.ts +++ b/extensions/crossmodel-lang/src/language-server/generated/ast.ts @@ -39,8 +39,10 @@ export function isCrossModelRoot(item: unknown): item is CrossModelRoot { export interface DiagramEdge extends AstNode { readonly $container: SystemDiagram; readonly $type: 'DiagramEdge'; - for?: Reference name?: string + relationship?: Reference + sourceNode?: Reference + targetNode?: Reference } export const DiagramEdge = 'DiagramEdge'; @@ -53,7 +55,7 @@ export interface DiagramNode extends AstNode { readonly $container: SystemDiagram; readonly $type: 'DiagramNode'; description?: string - for?: Reference + entity?: Reference height?: number name?: string name_val?: string @@ -158,10 +160,14 @@ export class CrossModelAstReflection extends AbstractAstReflection { getReferenceType(refInfo: ReferenceInfo): string { const referenceId = `${refInfo.container.$type}:${refInfo.property}`; switch (referenceId) { - case 'DiagramEdge:for': { + case 'DiagramEdge:relationship': { return Relationship; } - case 'DiagramNode:for': + case 'DiagramEdge:sourceNode': + case 'DiagramEdge:targetNode': { + return DiagramNode; + } + case 'DiagramNode:entity': case 'Relationship:child': case 'Relationship:parent': { return Entity; diff --git a/extensions/crossmodel-lang/src/language-server/generated/grammar.ts b/extensions/crossmodel-lang/src/language-server/generated/grammar.ts index 262c4711..c01f174b 100644 --- a/extensions/crossmodel-lang/src/language-server/generated/grammar.ts +++ b/extensions/crossmodel-lang/src/language-server/generated/grammar.ts @@ -953,7 +953,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "elements": [ { "$type": "Keyword", - "value": "for" + "value": "entity" }, { "$type": "Keyword", @@ -961,7 +961,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load }, { "$type": "Assignment", - "feature": "for", + "feature": "entity", "operator": "=", "terminal": { "$type": "CrossReference", @@ -1227,7 +1227,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "elements": [ { "$type": "Keyword", - "value": "for" + "value": "relationship" }, { "$type": "Keyword", @@ -1235,7 +1235,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load }, { "$type": "Assignment", - "feature": "for", + "feature": "relationship", "operator": "=", "terminal": { "$type": "CrossReference", @@ -1254,6 +1254,70 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load } ] }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "sourceNode" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "sourceNode", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@11" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@16" + }, + "arguments": [] + }, + "deprecatedSyntax": false + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "targetNode" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "targetNode", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@11" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@16" + }, + "arguments": [] + }, + "deprecatedSyntax": false + } + } + ] + }, { "$type": "Group", "elements": [ diff --git a/extensions/crossmodel-lang/src/model-server/model-server.ts b/extensions/crossmodel-lang/src/model-server/model-server.ts index 31517b61..eaa260ab 100644 --- a/extensions/crossmodel-lang/src/model-server/model-server.ts +++ b/extensions/crossmodel-lang/src/model-server/model-server.ts @@ -6,6 +6,7 @@ import { CloseModel, CrossModelRoot, OnSave, + OnUpdated, OpenModel, RequestModel, RequestModelDiagramNode, @@ -63,19 +64,19 @@ export class ModelServer implements Disposable { } } - const ref: Entity | undefined = diagramNode?.for?.ref; + const entity: Entity | undefined = diagramNode?.entity?.ref; - if (!diagramNode || !diagramNode.for || !ref || !ref.$container.$document) { + if (!entity?.$container.$document) { throw new Error('No node found with the given id'); } const serializedEntity = toSerializable({ $type: 'CrossModelRoot', - entity: ref + entity: entity }) as CrossModelRoot; return { - uri: ref.$container.$document.uri.toString(), + uri: entity.$container.$document.uri.toString(), model: serializedEntity }; } @@ -87,6 +88,10 @@ export class ModelServer implements Disposable { // TODO: Research if this also has to be closed after the document closes this.connection.sendNotification(OnSave, uri, toSerializable(newModel) as CrossModelRoot); }); + this.modelService.onUpdate(uri, newModel => { + // TODO: Research if this also has to be closed after the document closes + this.connection.sendNotification(OnUpdated, uri, toSerializable(newModel) as CrossModelRoot); + }); } protected async closeModel(uri: string): Promise { diff --git a/extensions/crossmodel-lang/src/model-server/model-service.ts b/extensions/crossmodel-lang/src/model-server/model-service.ts index 2f5e8b32..935598fd 100644 --- a/extensions/crossmodel-lang/src/model-server/model-service.ts +++ b/extensions/crossmodel-lang/src/model-server/model-service.ts @@ -4,7 +4,7 @@ import { AstNode, DocumentState, isAstNode } from 'langium'; import { Disposable } from 'vscode-languageserver'; -import { TextDocument } from 'vscode-languageserver-textdocument'; +import { OptionalVersionedTextDocumentIdentifier, Range, TextDocumentEdit, TextEdit } from 'vscode-languageserver-protocol'; import { URI } from 'vscode-uri'; import { CrossModelSharedServices } from '../language-server/cross-model-module'; @@ -77,12 +77,28 @@ export class ModelService { if (!isAstNode(root)) { throw new Error(`No AST node to update exists in '${uri}'`); } - + const textDocument = document.textDocument; const text = typeof model === 'string' ? model : this.serialize(URI.parse(uri), model); - const version = document.textDocument.version + 1; + if (text === textDocument.getText()) { + return document.parseResult.value as T; + } + + if (this.documentManager.isOpenInTextEditor(uri)) { + // we only want to apply a text edit if the editor is already open + // because opening and updating at the same time might cause problems as the open call resets the document to filesystem + await this.shared.lsp.Connection?.workspace.applyEdit({ + label: 'Update Model', + documentChanges: [ + // we use a null version to indicate that the version is known + // eslint-disable-next-line no-null/no-null + TextDocumentEdit.create(OptionalVersionedTextDocumentIdentifier.create(textDocument.uri, null), [ + TextEdit.replace(Range.create(0, 0, textDocument.lineCount, textDocument.getText().length), text) + ]) + ] + }); + } - TextDocument.update(document.textDocument, [{ text }], version); - await this.documentManager.update(uri, version, text); + await this.documentManager.update(uri, textDocument.version + 1, text); await this.documentBuilder.update([URI.parse(uri)], []); return document.parseResult.value as T; @@ -108,6 +124,11 @@ export class ModelService { * @param model semantic model or text */ async save(uri: string, model: AstNode | string): Promise { + // sync: implicit update of internal data structure to match file system (similar to workspace initialization) + if (this.documents.hasDocument(URI.parse(uri))) { + await this.update(uri, model); + } + const text = typeof model === 'string' ? model : this.serialize(URI.parse(uri), model); return this.documentManager.save(uri, text); } diff --git a/extensions/crossmodel-lang/src/model-server/open-text-document-manager.ts b/extensions/crossmodel-lang/src/model-server/open-text-document-manager.ts index 57333e5d..df7a2056 100644 --- a/extensions/crossmodel-lang/src/model-server/open-text-document-manager.ts +++ b/extensions/crossmodel-lang/src/model-server/open-text-document-manager.ts @@ -4,12 +4,12 @@ import * as fs from 'fs'; import { AstNode, FileSystemProvider, LangiumDefaultSharedServices, LangiumDocuments } from 'langium'; +import { Disposable } from 'vscode-languageserver'; import { TextDocumentIdentifier, TextDocumentItem, VersionedTextDocumentIdentifier } from 'vscode-languageserver-protocol'; import { TextDocument } from 'vscode-languageserver-textdocument'; import { URI } from 'vscode-uri'; import { AddedSharedModelServices } from './model-module'; import { OpenableTextDocuments } from './openable-text-documents'; -import { Disposable } from 'vscode-languageserver'; /** * A manager class that suppors handling documents with a simple open-update-save/close lifecycle. @@ -62,7 +62,7 @@ export class OpenTextDocumentManager { } this.openDocuments.push(this.normalizedUri(uri)); const textDocument = await this.readFromFilesystem(uri, languageId); - this.textDocuments.notifyDidOpenTextDocument({ textDocument }); + this.textDocuments.notifyDidOpenTextDocument({ textDocument }, false); } async close(uri: string): Promise { @@ -86,13 +86,17 @@ export class OpenTextDocumentManager { async save(uri: string, text: string): Promise { const vscUri = URI.parse(uri); fs.writeFileSync(vscUri.fsPath, text); - this.textDocuments.notifyDidSaveTextDocument({ textDocument: TextDocumentIdentifier.create(uri) }); + this.textDocuments.notifyDidSaveTextDocument({ textDocument: TextDocumentIdentifier.create(uri), text }); } - protected isOpen(uri: string): boolean { + isOpen(uri: string): boolean { return this.openDocuments.includes(this.normalizedUri(uri)); } + isOpenInTextEditor(uri: string): boolean { + return this.textDocuments.isOpenInTextEditor(this.normalizedUri(uri)); + } + protected removeFromOpenedDocuments(uri: string): void { this.openDocuments.splice(this.openDocuments.indexOf(this.normalizedUri(uri))); } diff --git a/extensions/crossmodel-lang/src/model-server/openable-text-documents.ts b/extensions/crossmodel-lang/src/model-server/openable-text-documents.ts index 3f6ebc66..595b9057 100644 --- a/extensions/crossmodel-lang/src/model-server/openable-text-documents.ts +++ b/extensions/crossmodel-lang/src/model-server/openable-text-documents.ts @@ -10,7 +10,6 @@ import { DidOpenTextDocumentParams, DidSaveTextDocumentParams, Disposable, - DocumentUri, Emitter, HandlerResult, RequestHandler, @@ -22,6 +21,9 @@ import { TextEdit, WillSaveTextDocumentParams } from 'vscode-languageserver'; +import { TextDocument } from 'vscode-languageserver-textdocument'; + +const OPENED_IN_TEXT_EDITOR_PROP = '_openedInTextEditor'; /** * This subclass of `TextDocuments` is actually entirely equivalent to `TextDocuments` but opens up @@ -31,7 +33,7 @@ import { * langium/src/workspace/documents.ts:222 which relies on _syncedDocuments to be open * vscode-languageserver/lib/common/textDocuments.js:119 */ -export class OpenableTextDocuments extends TextDocuments { +export class OpenableTextDocuments extends TextDocuments { public constructor(protected configuration: TextDocumentsConfiguration) { super(configuration); } @@ -117,6 +119,10 @@ export class OpenableTextDocuments extends TextD let syncedDocument = this.__syncedDocuments.get(td.uri); if (syncedDocument !== undefined) { + if (syncedDocument.version >= td.version) { + console.log(`Skip update of document ${td.uri} as local version is newer (${syncedDocument.version} >= ${td.version})`); + return; + } syncedDocument = this.configuration.update(syncedDocument, changes, version); this.__syncedDocuments.set(td.uri, syncedDocument); this.__onDidChangeContent.fire(Object.freeze({ document: syncedDocument })); @@ -157,12 +163,25 @@ export class OpenableTextDocuments extends TextD } } - public notifyDidOpenTextDocument(event: DidOpenTextDocumentParams): void { + public notifyDidOpenTextDocument(event: DidOpenTextDocumentParams, openedInTextEditor = true): void { const td = event.textDocument; const document = this.configuration.create(td.uri, td.languageId, td.version, td.text); + const wasOpenInTextEditor = this.isOpenInTextEditor(td.uri); this.__syncedDocuments.set(td.uri, document); + this.markOpenInTextEditor(td.uri, wasOpenInTextEditor || openedInTextEditor); const toFire = Object.freeze({ document }); this.__onDidOpen.fire(toFire); this.__onDidChangeContent.fire(toFire); } + + isOpenInTextEditor(uri: string): boolean { + return !!(this.__syncedDocuments.get(uri) as any | undefined)?.[OPENED_IN_TEXT_EDITOR_PROP]; + } + + protected markOpenInTextEditor(uri: string, open: boolean): void { + const document = this.__syncedDocuments.get(uri); + if (document) { + (document as any)[OPENED_IN_TEXT_EDITOR_PROP] = open; + } + } } diff --git a/extensions/crossmodel-lang/syntaxes/cross-model.tmLanguage.json b/extensions/crossmodel-lang/syntaxes/cross-model.tmLanguage.json index 4365b646..aa676d7e 100644 --- a/extensions/crossmodel-lang/syntaxes/cross-model.tmLanguage.json +++ b/extensions/crossmodel-lang/syntaxes/cross-model.tmLanguage.json @@ -10,7 +10,7 @@ }, { "name": "keyword.control.cross-model", - "match": "\\b(attributes|child|datatype|description|diagram|edges|entity|for|height|id|name|nodes|parent|relationship|type|width|x|y)\\b" + "match": "\\b(attributes|child|datatype|description|diagram|edges|entity|height|id|name|nodes|parent|relationship|sourceNode|targetNode|type|width|x|y)\\b" }, { "name": "string.quoted.double.cross-model", diff --git a/extensions/crossmodel-lang/test/language-server/cross-model-lang-diagram.test.ts b/extensions/crossmodel-lang/test/language-server/cross-model-lang-diagram.test.ts index 8af73ac2..ebd10d3c 100644 --- a/extensions/crossmodel-lang/test/language-server/cross-model-lang-diagram.test.ts +++ b/extensions/crossmodel-lang/test/language-server/cross-model-lang-diagram.test.ts @@ -4,11 +4,11 @@ import { describe, expect, test } from '@jest/globals'; import { EmptyFileSystem, isReference } from 'langium'; -import { parseDocument } from './test-utils/utils'; import { diagram1, diagram2, diagram3, diagram4, diagram5, diagram6 } from './test-utils/test-documents/diagram/index'; +import { parseDocument } from './test-utils/utils'; -import { CrossModelRoot } from '../../src/language-server/generated/ast'; import { createCrossModelServices } from '../../src/language-server/cross-model-module'; +import { CrossModelRoot } from '../../src/language-server/generated/ast'; const services = createCrossModelServices({ ...EmptyFileSystem }).CrossModel; @@ -51,8 +51,8 @@ describe('CrossModel language Diagram', () => { expect(model.diagram?.nodes.length).toBe(1); expect(node1?.name).toBe('CustomerNode'); - expect(isReference(node1?.for)).toBe(true); - expect(node1?.for?.$refText).toBe('Customer'); + expect(isReference(node1?.entity)).toBe(true); + expect(node1?.entity?.$refText).toBe('Customer'); expect(node1?.x).toBe(100); }); }); @@ -71,8 +71,8 @@ describe('CrossModel language Diagram', () => { expect(model.diagram?.edges.length).toBe(1); expect(edge1?.name).toBe('OrderCustomerEdge'); - expect(isReference(edge1?.for)).toBe(true); - expect(edge1?.for?.$refText).toBe('Order_Customer'); + expect(isReference(edge1?.relationship)).toBe(true); + expect(edge1?.relationship?.$refText).toBe('Order_Customer'); }); }); @@ -95,13 +95,13 @@ describe('CrossModel language Diagram', () => { expect(model.diagram?.edges.length).toBe(1); expect(node1?.name).toBe('CustomerNode'); - expect(isReference(node1?.for)).toBe(true); - expect(node1?.for?.$refText).toBe('Customer'); + expect(isReference(node1?.entity)).toBe(true); + expect(node1?.entity?.$refText).toBe('Customer'); expect(node1?.x).toBe(100); expect(edge1?.name).toBe('OrderCustomerEdge'); - expect(isReference(edge1?.for)).toBe(true); - expect(edge1?.for?.$refText).toBe('Order_Customer'); + expect(isReference(edge1?.relationship)).toBe(true); + expect(edge1?.relationship?.$refText).toBe('Order_Customer'); }); test('Simple file for diagram and edges, but descirption and name coming last', async () => { @@ -121,13 +121,13 @@ describe('CrossModel language Diagram', () => { expect(model.diagram?.edges.length).toBe(1); expect(node1?.name).toBe('CustomerNode'); - expect(isReference(node1?.for)).toBe(true); - expect(node1?.for?.$refText).toBe('Customer'); + expect(isReference(node1?.entity)).toBe(true); + expect(node1?.entity?.$refText).toBe('Customer'); expect(node1?.x).toBe(100); expect(edge1?.name).toBe('OrderCustomerEdge'); - expect(isReference(edge1?.for)).toBe(true); - expect(edge1?.for?.$refText).toBe('Order_Customer'); + expect(isReference(edge1?.relationship)).toBe(true); + expect(edge1?.relationship?.$refText).toBe('Order_Customer'); }); }); }); diff --git a/extensions/crossmodel-lang/test/language-server/serializer/cross-model-serializer.test.ts b/extensions/crossmodel-lang/test/language-server/serializer/cross-model-serializer.test.ts index c103552d..d6d387c6 100644 --- a/extensions/crossmodel-lang/test/language-server/serializer/cross-model-serializer.test.ts +++ b/extensions/crossmodel-lang/test/language-server/serializer/cross-model-serializer.test.ts @@ -2,13 +2,13 @@ * Copyright (c) 2023 CrossBreeze. ********************************************************************************/ -import { describe, test, beforeAll, expect } from '@jest/globals'; +import { beforeAll, describe, expect, test } from '@jest/globals'; import { EmptyFileSystem, Reference } from 'langium'; +import _ from 'lodash'; import { createCrossModelServices } from '../../../src/language-server/cross-model-module'; import { CrossModelSerializer } from '../../../src/language-server/cross-model-serializer'; import { CrossModelRoot, Entity, Relationship } from '../../../src/language-server/generated/ast'; -import _ from 'lodash'; const services = createCrossModelServices({ ...EmptyFileSystem }).CrossModel; @@ -193,7 +193,7 @@ describe('CrossModelLexer', () => { y: 101, width: 102, height: 102, - for: ref1, + entity: ref1, name: 'Node1', name_val: 'Node 1' }, @@ -204,7 +204,7 @@ describe('CrossModelLexer', () => { y: 101, width: 102, height: 102, - for: ref2, + entity: ref2, name: 'Node2', name_val: 'Node 2' } @@ -214,7 +214,7 @@ describe('CrossModelLexer', () => { { $container: crossModelRoot.diagram, $type: 'DiagramEdge', - for: ref3, + relationship: ref3, name: 'Edge1' } ]; @@ -228,53 +228,54 @@ describe('CrossModelLexer', () => { }); const expected_result = `entity: - description: "Test description" id: "test id" name: "test Name" + description: "Test description" attributes: - id: "Attribute 1" datatype: "Datatype Attribute 1" - id: "Attribute 2" datatype: "Datatype Attribute 2"`; const expected_result2 = `entity: - description: "Test description" id: "test id" - name: "test Name"`; + name: "test Name" + description: "Test description"`; const expected_result3 = `entity: + id: "test id" + name: "test Name" description: "Test description" attributes: - id: "Attribute 1" datatype: "Datatype Attribute 1" - id: "Attribute 2" - datatype: "Datatype Attribute 2" - id: "test id" - name: "test Name"`; + datatype: "Datatype Attribute 2"`; + const expected_result4 = `relationship: - description: "Test description" id: "test id" name: "test Name" + description: "Test description" parent: "Ref1" child: "Ref2" type: "n:m"`; const expected_result5 = `diagram: - description: "Test description" id: "test id" name: "test Name" + description: "Test description" nodes: - - x: 100 + - id: "Node1" + name: "Node 1" + entity: "Ref1" + x: 100 y: 101 width: 102 height: 102 - for: "Ref1" - id: "Node1" - name: "Node 1" - - x: 100 + - id: "Node2" + name: "Node 2" + entity: "Ref2" + x: 100 y: 101 width: 102 height: 102 - for: "Ref2" - id: "Node2" - name: "Node 2" edges: - - for: "Ref3" - id: "Edge1"`; + - id: "Edge1" + relationship: "Ref3"`; diff --git a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram2.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram2.ts index 8c6b41f9..b4027f25 100644 --- a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram2.ts +++ b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram2.ts @@ -5,7 +5,7 @@ export const diagram2 = `diagram: id: "Systemdiagram1" nodes: - id: 'CustomerNode' - for: 'Customer' + entity: 'Customer' x: 100 y: 100 height: 100 diff --git a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram3.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram3.ts index 8e204013..61ae46a4 100644 --- a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram3.ts +++ b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram3.ts @@ -5,4 +5,4 @@ export const diagram3 = `diagram: id: "Systemdiagram1" edges: - id: 'OrderCustomerEdge' - for: 'Order_Customer'`; + relationship: 'Order_Customer'`; diff --git a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram5.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram5.ts index 4155ae39..901abb8a 100644 --- a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram5.ts +++ b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram5.ts @@ -7,10 +7,10 @@ export const diagram5 = `diagram: description: "This is a basic diagram with nodes and edges" edges: - id: 'OrderCustomerEdge' - for: 'Order_Customer' + relationship: 'Order_Customer' nodes: - id: 'CustomerNode' - for: 'Customer' + entity: 'Customer' x: 100 y: 100 height: 100 diff --git a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram6.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram6.ts index d8cab3b3..4692cf1b 100644 --- a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram6.ts +++ b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram6.ts @@ -5,10 +5,10 @@ export const diagram6 = `diagram: id: "Systemdiagram1" edges: - id: 'OrderCustomerEdge' - for: 'Order_Customer' + relationship: 'Order_Customer' nodes: - id: 'CustomerNode' - for: 'Customer' + entity: 'Customer' x: 100 y: 100 height: 100 diff --git a/packages/form-client/src/browser/form-editor-widget.tsx b/packages/form-client/src/browser/form-editor-widget.tsx index b55175a5..4589690a 100644 --- a/packages/form-client/src/browser/form-editor-widget.tsx +++ b/packages/form-client/src/browser/form-editor-widget.tsx @@ -2,16 +2,17 @@ * Copyright (c) 2023 CrossBreeze. ********************************************************************************/ +import { ModelService, ModelServiceClient } from '@crossbreeze/model-service/lib/common'; import { CrossModelRoot } from '@crossbreeze/protocol'; import { CommandService, Emitter, Event } from '@theia/core'; import { LabelProvider, NavigatableWidget, NavigatableWidgetOptions, ReactWidget, SaveOptions, Saveable } from '@theia/core/lib/browser'; import URI from '@theia/core/lib/common/uri'; import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import * as React from '@theia/core/shared/react'; - -import { ModelService, ModelServiceClient } from '@crossbreeze/model-service/lib/common'; import '../../style/form-view.css'; import { App } from './react-components/App'; +import debounce = require('p-debounce'); +import deepEqual = require('fast-deep-equal'); export const FormEditorWidgetOptions = Symbol('FormEditorWidgetOptions'); export interface FormEditorWidgetOptions extends NavigatableWidgetOptions { @@ -24,7 +25,6 @@ export class FormEditorWidget extends ReactWidget implements NavigatableWidget, autoSave: 'off' | 'afterDelay' | 'onFocusChange' | 'onWindowChange'; public readonly onDirtyChangedEmitter = new Emitter(); onDirtyChanged: Event = this.onDirtyChangedEmitter.event; - saveUpdate = false; @inject(FormEditorWidgetOptions) protected options: FormEditorWidgetOptions; @inject(LabelProvider) protected labelProvider: LabelProvider; @@ -32,7 +32,7 @@ export class FormEditorWidget extends ReactWidget implements NavigatableWidget, @inject(CommandService) protected commandService: CommandService; @inject(ModelServiceClient) protected formClient: ModelServiceClient; - protected model: CrossModelRoot | undefined = undefined; + protected syncedModel: CrossModelRoot | undefined = undefined; protected error: string | undefined = undefined; @postConstruct() @@ -46,6 +46,12 @@ export class FormEditorWidget extends ReactWidget implements NavigatableWidget, this.updateModel = this.updateModel.bind(this); this.getResourceUri = this.getResourceUri.bind(this); this.loadModel(); + + this.formClient.onUpdate(document => { + if (document.uri === this.getResourceUri().toString()) { + this.modelUpdated(document.model); + } + }); } protected async loadModel(): Promise { @@ -54,7 +60,7 @@ export class FormEditorWidget extends ReactWidget implements NavigatableWidget, await this.modelService.open(uri); const model = await this.modelService.request(uri); if (model) { - this.model = model; + this.syncedModel = model; } } catch (error: any) { this.error = error; @@ -64,27 +70,26 @@ export class FormEditorWidget extends ReactWidget implements NavigatableWidget, } async save(options?: SaveOptions | undefined): Promise { - if (this.model === undefined) { + if (this.syncedModel === undefined) { throw new Error('Cannot save undefined model'); } this.setDirty(false); - // When the model on the model-server is updated we will get a notification that the model has been saved. - // This variable lets us know that we were the ones that saved the model - this.saveUpdate = true; - - await this.modelService.save(this.getResourceUri().toString(), this.model); + await this.modelService.save(this.getResourceUri().toString(), this.syncedModel); } - protected async updateModel(model: CrossModelRoot): Promise { - // If we were the ones that send the save request, we do not want to update the model again - if (this.saveUpdate) { - this.saveUpdate = false; - return; + protected updateModel = debounce((model: CrossModelRoot) => { + if (!deepEqual(this.syncedModel, model)) { + this.syncedModel = model; + this.modelService.update(this.getResourceUri().toString(), model); } + }, 200); - this.model = model; - await this.modelService.update(this.getResourceUri().toString(), this.model!); + protected modelUpdated(model: CrossModelRoot): void { + if (!deepEqual(this.syncedModel, model)) { + this.syncedModel = model; + this.update(); + } } override close(): void { @@ -103,7 +108,7 @@ export class FormEditorWidget extends ReactWidget implements NavigatableWidget, render(): React.ReactNode { const props = { - model: this.model, + model: this.syncedModel, updateModel: this.updateModel, getResourceUri: this.getResourceUri, formClient: this.formClient diff --git a/packages/form-client/src/browser/react-components/App.tsx b/packages/form-client/src/browser/react-components/App.tsx index e3911c59..2375f8d0 100644 --- a/packages/form-client/src/browser/react-components/App.tsx +++ b/packages/form-client/src/browser/react-components/App.tsx @@ -2,13 +2,13 @@ * Copyright (c) 2023 CrossBreeze. ********************************************************************************/ +import { ModelServiceClient } from '@crossbreeze/model-service/lib/common'; import { CrossModelRoot } from '@crossbreeze/protocol'; import URI from '@theia/core/lib/common/uri'; import * as React from '@theia/core/shared/react'; -import { ModelProvider, ModelReducer } from './ModelContext'; +import { AppState, ModelProvider, ModelReducer } from './ModelContext'; import { EntityForm } from './entity-components/EntityForm'; import _ = require('lodash'); -import { ModelServiceClient } from '@crossbreeze/model-service/lib/common'; interface AppProps { updateModel: (model: CrossModelRoot) => void; @@ -17,30 +17,27 @@ interface AppProps { formClient: ModelServiceClient; } -export function App(props: AppProps): React.ReactElement { - const [model, dispatch] = React.useReducer(ModelReducer, props.model as CrossModelRoot); +export function App({ model, updateModel }: AppProps): React.ReactElement { + const [appState, dispatch] = React.useReducer(ModelReducer, { model, reason: '' } as AppState); - // Subscribing to the updates made to the model by a different editor React.useEffect(() => { - props.formClient.onUpdate(document => { - if (document.uri === props.getResourceUri().toString()) { - dispatch({ type: 'model:update', model: document.model }); - } - }); - }, [props]); + // triggered when a new model is passed from the outside (widget) -> update internal state + dispatch({ type: 'model:update', model: model }); + }, [model]); - // This effect gets triggered when the model gets updated, it will pass the new model - // to the Form-widget and that will pass it to the server to update React.useEffect(() => { - props.updateModel(_.cloneDeep(model)); - }, [model, props]); + if (appState.reason !== 'model:update') { + // triggered when the internal model is updated, pass update to server + updateModel(_.cloneDeep(appState.model)); + } + }, [appState, updateModel]); let render = undefined; // Rendering logic - if (!model) { + if (!appState?.model) { render =
loading
; - } else if (model.entity) { + } else if (appState.model.entity) { render = ; } else { render = ( @@ -58,7 +55,7 @@ export function App(props: AppProps): React.ReactElement { return (
- + {render}
diff --git a/packages/form-client/src/browser/react-components/ModelContext.tsx b/packages/form-client/src/browser/react-components/ModelContext.tsx index 4fb5d884..0e3b1bee 100644 --- a/packages/form-client/src/browser/react-components/ModelContext.tsx +++ b/packages/form-client/src/browser/react-components/ModelContext.tsx @@ -1,8 +1,8 @@ /******************************************************************************** * Copyright (c) 2023 CrossBreeze. ********************************************************************************/ -import * as React from '@theia/core/shared/react'; import { CrossModelRoot } from '@crossbreeze/protocol'; +import * as React from '@theia/core/shared/react'; import _ = require('lodash'); /** @@ -43,7 +43,12 @@ export function ModelProvider(props: ModelProviderProps): React.ReactElement { ); } -export function ModelReducer(model: CrossModelRoot, action: any): CrossModelRoot { +export interface AppState { + model: CrossModelRoot; + reason: string; +} + +export function ModelReducer({ model }: AppState, action: any): AppState { if (model === undefined) { throw Error('Model error: model.entity undefined'); } @@ -55,7 +60,7 @@ export function ModelReducer(model: CrossModelRoot, action: any): CrossModelRoot switch (action.type) { // Update the entire model case 'model:update': - return action.model; + return { model: action.model, reason: action.type }; // Change the name of the entity-model case 'entity:change-name': @@ -66,7 +71,7 @@ export function ModelReducer(model: CrossModelRoot, action: any): CrossModelRoot } model.entity.name = action.name; - return model; + return { model, reason: action.type }; // Change the name of the entity-model case 'entity:change-description': @@ -77,7 +82,7 @@ export function ModelReducer(model: CrossModelRoot, action: any): CrossModelRoot } model.entity.description = action.description; - return model; + return { model, reason: action.type }; // Change the datatype of one of entity attributes case 'entity:attribute:change-datatype': @@ -89,7 +94,7 @@ export function ModelReducer(model: CrossModelRoot, action: any): CrossModelRoot model.entity.attributes[action.id].datatype = action.dataType; - return model; + return { model, reason: action.type }; // Change the name of one of entity attributes case 'entity:attribute:change-name': @@ -101,7 +106,7 @@ export function ModelReducer(model: CrossModelRoot, action: any): CrossModelRoot model.entity.attributes[action.id].name = action.name; - return model; + return { model, reason: action.type }; default: { throw Error('Unknown ModelReducer action'); diff --git a/packages/model-service/src/node/model-service.ts b/packages/model-service/src/node/model-service.ts index 8e6e0d57..8e81a65d 100644 --- a/packages/model-service/src/node/model-service.ts +++ b/packages/model-service/src/node/model-service.ts @@ -8,6 +8,7 @@ import { DiagramNodeEntity, MODELSERVER_PORT_FILE, OnSave, + OnUpdated, OpenModel, PORT_FOLDER, RequestModel, @@ -67,9 +68,7 @@ export class ModelServiceImpl implements ModelService, BackendApplicationContrib socket.connect({ port }); this.connection.listen(); - this.connection.onNotification(OnSave, (uri: string, model: CrossModelRoot) => { - this.client?.updateModel(uri, model); - }); + this.setUpListeners(); return connected.promise; } @@ -127,6 +126,9 @@ export class ModelServiceImpl implements ModelService, BackendApplicationContrib this.connection.onNotification(OnSave, (uri, model) => { this.client?.updateModel(uri, model); }); + this.connection.onNotification(OnUpdated, (uri, model) => { + this.client?.updateModel(uri, model); + }); } setClient(client: ModelServiceClient): void { diff --git a/packages/protocol/src/model-service/protocol.ts b/packages/protocol/src/model-service/protocol.ts index b95c145d..3ce56a2d 100644 --- a/packages/protocol/src/model-service/protocol.ts +++ b/packages/protocol/src/model-service/protocol.ts @@ -61,3 +61,4 @@ export const RequestModelDiagramNode = new rpc.RequestType2('server/update'); export const SaveModel = new rpc.RequestType2('server/save'); export const OnSave = new rpc.NotificationType2('server/onSave'); +export const OnUpdated = new rpc.NotificationType2('server/onUpdated'); From 639466e9e0c18eab3f744db801f1775863531312 Mon Sep 17 00:00:00 2001 From: Martin Fleck Date: Fri, 6 Oct 2023 11:48:53 +0200 Subject: [PATCH 14/22] Update example files and fix IDs for GLSP server for better update --- .../voorbeeld_taal_entity_customer.cm | 26 +++++++++---------- .../entities/voorbeeld_taal_entity_order.cm | 20 +++++++------- .../source/voorbeeld_taal_diagram.diagram.cm | 5 ++-- .../model/builders/node-builder.ts | 7 ++++- .../glsp-server/model/cross-model-storage.ts | 10 ++++++- .../src/browser/model-property-widget.tsx | 7 ++--- 6 files changed, 43 insertions(+), 32 deletions(-) diff --git a/examples/yaml-example/entities/voorbeeld_taal_entity_customer.cm b/examples/yaml-example/entities/voorbeeld_taal_entity_customer.cm index ac159308..72772eb6 100644 --- a/examples/yaml-example/entities/voorbeeld_taal_entity_customer.cm +++ b/examples/yaml-example/entities/voorbeeld_taal_entity_customer.cm @@ -1,23 +1,23 @@ entity: - name: "Customer" id: "Customer" + name: "Customer" description: "A customer with whom a transaction has been made." attributes: - - name: "Id" - id: "Id" + - id: "Id" + name: "Id" datatype: "Integer" - - name: "FirstName" - id: "FirstName" + - id: "FirstName" + name: "FirstName" datatype: "Varchar" - - name: "LastName" - id: "LastName" + - id: "LastName" + name: "LastName" datatype: "Varchar" - - name: "City" - id: "City" + - id: "City" + name: "City" datatype: "Varchar" - - name: "Country" - id: "Country" + - id: "Country" + name: "Country" datatype: "Varchar" - - name: "Phone" - id: "Phone" + - id: "Phone" + name: "Phone" datatype: "Varchar" \ No newline at end of file diff --git a/examples/yaml-example/entities/voorbeeld_taal_entity_order.cm b/examples/yaml-example/entities/voorbeeld_taal_entity_order.cm index 1f989673..5fa73c2b 100644 --- a/examples/yaml-example/entities/voorbeeld_taal_entity_order.cm +++ b/examples/yaml-example/entities/voorbeeld_taal_entity_order.cm @@ -2,18 +2,18 @@ entity: id: "Order" description: "Order placed by a customer in the Customer table." attributes: - - name: "Id" - id: "Id" + - id: "Id" + name: "Id" datatype: "Integer" - - name: "OrderDate" - id: "OrderDate" + - id: "OrderDate" + name: "OrderDate" datatype: "Integer" - - name: "OrderNumber" - id: "OrderNumber" + - id: "OrderNumber" + name: "OrderNumber" datatype: "Varchar" - - name: "CustomerId" - id: "CustomerId" + - id: "CustomerId" + name: "CustomerId" datatype: "Integer" - - name: "TotalAmount" - id: "TotalAmount" + - id: "TotalAmount" + name: "TotalAmount" datatype: "Float" \ No newline at end of file diff --git a/examples/yaml-example/source/voorbeeld_taal_diagram.diagram.cm b/examples/yaml-example/source/voorbeeld_taal_diagram.diagram.cm index 420cb591..21bcd69c 100644 --- a/examples/yaml-example/source/voorbeeld_taal_diagram.diagram.cm +++ b/examples/yaml-example/source/voorbeeld_taal_diagram.diagram.cm @@ -1,5 +1,6 @@ diagram: id: "Systemdiagram1" + name: "Systemdiagram1" description: "This should be the description" nodes: - id: "CustomerNode" @@ -10,8 +11,8 @@ diagram: height: 151 - id: "OrderNode" entity: "Order" - x: 655.4416344093675 - y: 270.85224527770106 + x: 618.4416344093675 + y: 274.85224527770106 width: 139.6079559326172 height: 132 edges: diff --git a/extensions/crossmodel-lang/src/glsp-server/model/builders/node-builder.ts b/extensions/crossmodel-lang/src/glsp-server/model/builders/node-builder.ts index 504636ac..828b2b4f 100644 --- a/extensions/crossmodel-lang/src/glsp-server/model/builders/node-builder.ts +++ b/extensions/crossmodel-lang/src/glsp-server/model/builders/node-builder.ts @@ -27,6 +27,7 @@ export class GEntityNodeBuilder extends GNodeBuilder { // Add the label/name of the node const label = GCompartment.builder() + .id(`${this.proxy.id}_header`) .layout('hbox') .addLayoutOption('hAlign', 'center') .addCssClass('entity-header-compartment') @@ -44,6 +45,7 @@ export class GEntityNodeBuilder extends GNodeBuilder { // Add the children of the node if (entityRef !== undefined) { const allAttributesCompartment = GCompartment.builder() + .id(`${this.proxy.id}_attributes`) .addCssClass('attributes-compartment') .layout('vbox') .addLayoutOption('hAlign', 'left') @@ -52,6 +54,7 @@ export class GEntityNodeBuilder extends GNodeBuilder { // Add the attributes of the entity. for (const attribute of entityRef.attributes) { const attributeCompartment = GCompartment.builder() + .id(`${this.proxy.id}_${attribute.name}_attribute`) .addCssClass('attribute-compartment') .layout('hbox') .addLayoutOption('paddingBottom', 3) @@ -59,13 +62,15 @@ export class GEntityNodeBuilder extends GNodeBuilder { attributeCompartment.add( GLabel.builder() + .id(`${this.proxy.id}_${attribute.name}_attribute_name`) .text(attribute.name_val || '') .addCssClass('attribute') .build() ); - attributeCompartment.add(GLabel.builder().text(' : ').build()); + attributeCompartment.add(GLabel.builder().text(' : ').id(`${this.proxy.id}_${attribute.name}_attribute_del`).build()); attributeCompartment.add( GLabel.builder() + .id(`${this.proxy.id}_${attribute.name}_attribute_type`) .text(attribute.datatype?.toString() || '') .addCssClass('datatype') .build() diff --git a/extensions/crossmodel-lang/src/glsp-server/model/cross-model-storage.ts b/extensions/crossmodel-lang/src/glsp-server/model/cross-model-storage.ts index be6cf167..6b492d0a 100644 --- a/extensions/crossmodel-lang/src/glsp-server/model/cross-model-storage.ts +++ b/extensions/crossmodel-lang/src/glsp-server/model/cross-model-storage.ts @@ -3,12 +3,14 @@ ********************************************************************************/ import { + ActionDispatcher, ClientSession, ClientSessionListener, ClientSessionManager, GLSPServerError, Logger, MaybePromise, + ModelSubmissionHandler, RequestModelAction, SOURCE_URI_ARG, SaveModelAction, @@ -17,7 +19,7 @@ import { import { inject, injectable, postConstruct } from 'inversify'; import { findRootNode, streamReferences } from 'langium'; import { URI } from 'vscode-uri'; -import { isCrossModelRoot } from '../../language-server/generated/ast'; +import { CrossModelRoot, isCrossModelRoot } from '../../language-server/generated/ast'; import { CrossModelState } from './cross-model-state'; /** @@ -30,6 +32,8 @@ export class CrossModelStorage implements SourceModelStorage, ClientSessionListe @inject(Logger) protected logger: Logger; @inject(CrossModelState) protected state: CrossModelState; @inject(ClientSessionManager) protected sessionManager: ClientSessionManager; + @inject(ModelSubmissionHandler) protected submissionHandler: ModelSubmissionHandler; + @inject(ActionDispatcher) protected actionDispatcher: ActionDispatcher; @postConstruct() protected init(): void { @@ -45,6 +49,10 @@ export class CrossModelStorage implements SourceModelStorage, ClientSessionListe throw new GLSPServerError('Expected CrossModal Diagram Root'); } this.state.setSemanticRoot(rootUri, root); + this.state.modelService.onUpdate(rootUri, async newRoot => { + this.state.setSemanticRoot(rootUri, newRoot); + this.actionDispatcher.dispatchAll(await this.submissionHandler.submitModel('external')); + }); } saveSourceModel(action: SaveModelAction): MaybePromise { diff --git a/packages/property-view/src/browser/model-property-widget.tsx b/packages/property-view/src/browser/model-property-widget.tsx index c1b128e1..93cd38f5 100644 --- a/packages/property-view/src/browser/model-property-widget.tsx +++ b/packages/property-view/src/browser/model-property-widget.tsx @@ -8,7 +8,7 @@ import { PropertyDataService } from '@theia/property-view/lib/browser/property-d import { PropertyViewContentWidget } from '@theia/property-view/lib/browser/property-view-content-widget'; import { ModelService } from '@crossbreeze/model-service/lib/common'; -import { CrossModelRoot, UpdateClientOperation, isDiagramNodeEntity } from '@crossbreeze/protocol'; +import { CrossModelRoot, isDiagramNodeEntity } from '@crossbreeze/protocol'; import { IActionDispatcher } from '@eclipse-glsp/client'; import { GLSPDiagramWidget, GlspSelection } from '@eclipse-glsp/theia-integration'; import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell'; @@ -60,10 +60,7 @@ export class ModelPropertyWidget extends ReactWidget implements PropertyViewCont if (this.model === undefined || this.uri === undefined) { throw new Error('Cannot save undefined model'); } - - const updated = await this.modelService.update(this.uri, this.model); - await this.modelService.save(this.uri, updated); - this.actionDispatcher?.dispatch(UpdateClientOperation.create()); + this.modelService.update(this.uri, this.model); } protected async updateModel(model: CrossModelRoot): Promise { From 15457f9d2a393adc6296601a3428f37b666f99a0 Mon Sep 17 00:00:00 2001 From: Martin Fleck Date: Wed, 11 Oct 2023 17:24:57 +0200 Subject: [PATCH 15/22] Extend protocol with client id and improve update mechanism - Use open/close as lifecycle methods for clients - Let extension startup early to handle non-textual editors --- extensions/crossmodel-lang/package.json | 2 +- .../handler/create-edge-operation-handler.ts | 2 +- .../crossmodel-lang/src/glsp-server/launch.ts | 2 +- .../glsp-server/model/cross-model-state.ts | 6 +- .../glsp-server/model/cross-model-storage.ts | 137 +++--- .../src/language-server/cross-model-module.ts | 2 +- .../src/model-server/model-server.ts | 74 +++- .../src/model-server/model-service.ts | 96 +++-- .../open-text-document-manager.ts | 110 +++-- .../model-server/openable-text-documents.ts | 407 ++++++++++-------- .../src/browser/form-editor-open-handler.ts | 18 +- .../src/browser/form-editor-widget.tsx | 44 +- .../tabs/EntityGeneralTab.tsx | 3 +- .../src/browser/model-service-client.ts | 13 +- .../src/common/model-service-rpc.ts | 25 +- .../model-service/src/node/model-service.ts | 30 +- .../src/browser/model-data-service.ts | 67 +++ .../src/browser/model-property-widget.tsx | 3 +- .../browser/property-view-frontend-module.ts | 6 +- .../src/common/model-data-service.ts | 49 --- .../protocol/src/model-service/protocol.ts | 43 +- 21 files changed, 684 insertions(+), 455 deletions(-) create mode 100644 packages/property-view/src/browser/model-data-service.ts delete mode 100644 packages/property-view/src/common/model-data-service.ts diff --git a/extensions/crossmodel-lang/package.json b/extensions/crossmodel-lang/package.json index fe8b7d83..04114a48 100644 --- a/extensions/crossmodel-lang/package.json +++ b/extensions/crossmodel-lang/package.json @@ -87,7 +87,7 @@ ] }, "activationEvents": [ - "onLanguage:cross-model" + "onStartupFinished" ], "dependencies": { "@crossbreeze/protocol": "0.0.0", diff --git a/extensions/crossmodel-lang/src/glsp-server/handler/create-edge-operation-handler.ts b/extensions/crossmodel-lang/src/glsp-server/handler/create-edge-operation-handler.ts index 636f55f9..93f148e6 100644 --- a/extensions/crossmodel-lang/src/glsp-server/handler/create-edge-operation-handler.ts +++ b/extensions/crossmodel-lang/src/glsp-server/handler/create-edge-operation-handler.ts @@ -84,7 +84,7 @@ export class CrossModelCreateEdgeOperationHandler extends OperationHandler imple relationshipRoot.relationship = relationship; const text = this.state.semanticSerializer.serialize(relationshipRoot); - await this.state.modelService.save(uri.toString(), text); + await this.state.modelService.save({ uri: uri.toString(), model: text, clientId: this.state.clientId }); const root = await this.state.modelService.request(uri.toString(), isCrossModelRoot); return root?.relationship; } diff --git a/extensions/crossmodel-lang/src/glsp-server/launch.ts b/extensions/crossmodel-lang/src/glsp-server/launch.ts index ebdeb6af..933a5722 100644 --- a/extensions/crossmodel-lang/src/glsp-server/launch.ts +++ b/extensions/crossmodel-lang/src/glsp-server/launch.ts @@ -25,7 +25,7 @@ import { CrossModelLayoutConfigurator } from './layout/cross-model-layout-config * @returns a promise that is resolved as soon as the server is shut down or rejects if an error occurs */ export function startGLSPServer(services: CrossModelLSPServices, workspaceFolder: URI): MaybePromise { - const launchOptions: SocketLaunchOptions = { ...defaultSocketLaunchOptions, logLevel: LogLevel.debug }; + const launchOptions: SocketLaunchOptions = { ...defaultSocketLaunchOptions, logLevel: LogLevel.info }; // create module based on launch options, e.g., logging etc. const appModule = createAppModule(launchOptions); diff --git a/extensions/crossmodel-lang/src/glsp-server/model/cross-model-state.ts b/extensions/crossmodel-lang/src/glsp-server/model/cross-model-state.ts index 500402f8..d1b6f924 100644 --- a/extensions/crossmodel-lang/src/glsp-server/model/cross-model-state.ts +++ b/extensions/crossmodel-lang/src/glsp-server/model/cross-model-state.ts @@ -60,7 +60,11 @@ export class CrossModelState extends DefaultModelState { } async updateSemanticRoot(content?: string): Promise { - this._semanticRoot = await this.modelService.update(this.semanticUri, content ?? this.semanticRoot); + this._semanticRoot = await this.modelService.update({ + uri: this.semanticUri, + model: content ?? this.semanticRoot, + clientId: this.clientId + }); this.index.indexSemanticRoot(this.semanticRoot); } diff --git a/extensions/crossmodel-lang/src/glsp-server/model/cross-model-storage.ts b/extensions/crossmodel-lang/src/glsp-server/model/cross-model-storage.ts index 6b492d0a..f258675b 100644 --- a/extensions/crossmodel-lang/src/glsp-server/model/cross-model-storage.ts +++ b/extensions/crossmodel-lang/src/glsp-server/model/cross-model-storage.ts @@ -3,18 +3,20 @@ ********************************************************************************/ import { - ActionDispatcher, - ClientSession, - ClientSessionListener, - ClientSessionManager, - GLSPServerError, - Logger, - MaybePromise, - ModelSubmissionHandler, - RequestModelAction, - SOURCE_URI_ARG, - SaveModelAction, - SourceModelStorage + ActionDispatcher, + ClientSession, + ClientSessionListener, + ClientSessionManager, + Disposable, + DisposableCollection, + GLSPServerError, + Logger, + MaybePromise, + ModelSubmissionHandler, + RequestModelAction, + SOURCE_URI_ARG, + SaveModelAction, + SourceModelStorage } from '@eclipse-glsp/server'; import { inject, injectable, postConstruct } from 'inversify'; import { findRootNode, streamReferences } from 'langium'; @@ -29,62 +31,71 @@ import { CrossModelState } from './cross-model-state'; */ @injectable() export class CrossModelStorage implements SourceModelStorage, ClientSessionListener { - @inject(Logger) protected logger: Logger; - @inject(CrossModelState) protected state: CrossModelState; - @inject(ClientSessionManager) protected sessionManager: ClientSessionManager; - @inject(ModelSubmissionHandler) protected submissionHandler: ModelSubmissionHandler; - @inject(ActionDispatcher) protected actionDispatcher: ActionDispatcher; + @inject(Logger) protected logger: Logger; + @inject(CrossModelState) protected state: CrossModelState; + @inject(ClientSessionManager) protected sessionManager: ClientSessionManager; + @inject(ModelSubmissionHandler) protected submissionHandler: ModelSubmissionHandler; + @inject(ActionDispatcher) protected actionDispatcher: ActionDispatcher; - @postConstruct() - protected init(): void { - this.sessionManager.addListener(this, this.state.clientId); - } + protected toDispose = new DisposableCollection(); - async loadSourceModel(action: RequestModelAction): Promise { - // load semantic model from document in language model service - const sourceUri = this.getSourceUri(action); - const rootUri = URI.file(sourceUri).toString(); - const root = await this.state.modelService.request(rootUri, isCrossModelRoot); - if (!root || !root.diagram) { - throw new GLSPServerError('Expected CrossModal Diagram Root'); - } - this.state.setSemanticRoot(rootUri, root); - this.state.modelService.onUpdate(rootUri, async newRoot => { - this.state.setSemanticRoot(rootUri, newRoot); - this.actionDispatcher.dispatchAll(await this.submissionHandler.submitModel('external')); - }); - } + @postConstruct() + protected init(): void { + this.sessionManager.addListener(this, this.state.clientId); + } - saveSourceModel(action: SaveModelAction): MaybePromise { - const saveUri = this.getFileUri(action); + async loadSourceModel(action: RequestModelAction): Promise { + // load semantic model from document in language model service + const sourceUri = this.getSourceUri(action); + const rootUri = URI.file(sourceUri).toString(); + await this.state.modelService.open({ uri: rootUri, clientId: this.state.clientId }); + this.toDispose.push(Disposable.create(() => this.state.modelService.close({ uri: rootUri, clientId: this.state.clientId }))); + const root = await this.state.modelService.request(rootUri, isCrossModelRoot); + if (!root || !root.diagram) { + throw new GLSPServerError('Expected CrossModal Diagram Root'); + } + this.state.setSemanticRoot(rootUri, root); + this.toDispose.push( + this.state.modelService.onUpdate(rootUri, async event => { + if (this.state.clientId !== event.sourceClientId) { + this.state.setSemanticRoot(rootUri, event.model); + this.actionDispatcher.dispatchAll(await this.submissionHandler.submitModel('external')); + } + }) + ); + } - // save document and all related documents - this.state.modelService.save(saveUri, this.state.semanticRoot); - streamReferences(this.state.semanticRoot) - .map(refInfo => refInfo.reference.ref) - .nonNullable() - .map(ref => findRootNode(ref)) - .forEach(root => this.state.modelService.save(root.$document!.uri.toString(), root)); - } + saveSourceModel(action: SaveModelAction): MaybePromise { + const saveUri = this.getFileUri(action); - sessionDisposed(_clientSession: ClientSession): void { - // close loaded document for modification - this.state.modelService.close(this.state.semanticUri); - } + // save document and all related documents + this.state.modelService.save({ uri: saveUri, model: this.state.semanticRoot, clientId: this.state.clientId }); + streamReferences(this.state.semanticRoot) + .map(refInfo => refInfo.reference.ref) + .nonNullable() + .map(ref => findRootNode(ref)) + .forEach(root => + this.state.modelService.save({ uri: root.$document!.uri.toString(), model: root, clientId: this.state.clientId }) + ); + } - protected getSourceUri(action: RequestModelAction): string { - const sourceUri = action.options?.[SOURCE_URI_ARG]; - if (typeof sourceUri !== 'string') { - throw new GLSPServerError(`Invalid RequestModelAction! Missing argument with key '${SOURCE_URI_ARG}'`); - } - return sourceUri; - } + sessionDisposed(_clientSession: ClientSession): void { + this.toDispose.dispose(); + } - protected getFileUri(action: SaveModelAction): string { - const uri = action.fileUri ?? this.state.get(SOURCE_URI_ARG); - if (!uri) { - throw new GLSPServerError('Could not derive fileUri for saving the current source model'); - } - return uri; - } + protected getSourceUri(action: RequestModelAction): string { + const sourceUri = action.options?.[SOURCE_URI_ARG]; + if (typeof sourceUri !== 'string') { + throw new GLSPServerError(`Invalid RequestModelAction! Missing argument with key '${SOURCE_URI_ARG}'`); + } + return sourceUri; + } + + protected getFileUri(action: SaveModelAction): string { + const uri = action.fileUri ?? this.state.get(SOURCE_URI_ARG); + if (!uri) { + throw new GLSPServerError('Could not derive fileUri for saving the current source model'); + } + return uri; + } } diff --git a/extensions/crossmodel-lang/src/language-server/cross-model-module.ts b/extensions/crossmodel-lang/src/language-server/cross-model-module.ts index f1d8841b..593c4283 100644 --- a/extensions/crossmodel-lang/src/language-server/cross-model-module.ts +++ b/extensions/crossmodel-lang/src/language-server/cross-model-module.ts @@ -95,7 +95,7 @@ export const CrossModelSharedModule: Module< WorkspaceManager: services => new CrossModelWorkspaceManager(services), PackageManager: services => new CrossModelPackageManager(services), LangiumDocuments: services => new CrossModelLangiumDocuments(services), - TextDocuments: () => new OpenableTextDocuments(TextDocument), + TextDocuments: services => new OpenableTextDocuments(TextDocument, services.logger.ClientLogger), TextDocumentManager: services => new OpenTextDocumentManager(services), DocumentBuilder: services => new CrossModelDocumentBuilder(services) }, diff --git a/extensions/crossmodel-lang/src/model-server/model-server.ts b/extensions/crossmodel-lang/src/model-server/model-server.ts index eaa260ab..a505c754 100644 --- a/extensions/crossmodel-lang/src/model-server/model-server.ts +++ b/extensions/crossmodel-lang/src/model-server/model-server.ts @@ -4,16 +4,20 @@ import { CloseModel, + CloseModelArgs, CrossModelRoot, OnSave, OnUpdated, OpenModel, + OpenModelArgs, RequestModel, RequestModelDiagramNode, SaveModel, - UpdateModel + SaveModelArgs, + UpdateModel, + UpdateModelArgs } from '@crossbreeze/protocol'; -import { AstNode, isReference } from 'langium'; +import { isReference } from 'langium'; import { Disposable } from 'vscode-jsonrpc'; import * as rpc from 'vscode-jsonrpc/node'; import { CrossModelRoot as CrossModelRootAst, DiagramNode, Entity, isCrossModelRoot } from '../language-server/generated/ast'; @@ -26,18 +30,19 @@ import { ModelService } from './model-service'; */ export class ModelServer implements Disposable { protected toDispose: Disposable[] = []; + protected toDisposeForSession: Map = new Map(); constructor(protected connection: rpc.MessageConnection, protected modelService: ModelService) { this.initialize(connection); } protected initialize(connection: rpc.MessageConnection): void { - this.toDispose.push(connection.onRequest(OpenModel, uri => this.openModel(uri))); - this.toDispose.push(connection.onRequest(CloseModel, uri => this.closeModel(uri))); + this.toDispose.push(connection.onRequest(OpenModel, args => this.openModel(args))); + this.toDispose.push(connection.onRequest(CloseModel, args => this.closeModel(args))); this.toDispose.push(connection.onRequest(RequestModel, uri => this.requestModel(uri))); this.toDispose.push(connection.onRequest(RequestModelDiagramNode, (uri, id) => this.requestModelDiagramNode(uri, id))); - this.toDispose.push(connection.onRequest(UpdateModel, (uri, model) => this.updateModel(uri, model))); - this.toDispose.push(connection.onRequest(SaveModel, (uri, model) => this.saveModel(uri, model))); + this.toDispose.push(connection.onRequest(UpdateModel, args => this.updateModel(args))); + this.toDispose.push(connection.onRequest(SaveModel, args => this.saveModel(args))); } /** @@ -81,21 +86,44 @@ export class ModelServer implements Disposable { }; } - protected async openModel(uri: string): Promise { - await this.modelService.open(uri); - - this.modelService.onSave(uri, newModel => { - // TODO: Research if this also has to be closed after the document closes - this.connection.sendNotification(OnSave, uri, toSerializable(newModel) as CrossModelRoot); - }); - this.modelService.onUpdate(uri, newModel => { - // TODO: Research if this also has to be closed after the document closes - this.connection.sendNotification(OnUpdated, uri, toSerializable(newModel) as CrossModelRoot); - }); + protected async openModel(args: OpenModelArgs): Promise { + if (!this.modelService.isOpen(args.uri)) { + await this.modelService.open(args); + } + this.setupListeners(args); + return this.requestModel(args.uri); + } + + protected setupListeners(args: OpenModelArgs): void { + this.disposeListeners(args); + const listenersForClient = []; + listenersForClient.push( + this.modelService.onSave(args.uri, event => + this.connection.sendNotification(OnSave, { + uri: args.uri, + model: toSerializable(event.model) as CrossModelRoot, + sourceClientId: event.sourceClientId + }) + ), + this.modelService.onUpdate(args.uri, event => + this.connection.sendNotification(OnUpdated, { + uri: args.uri, + model: toSerializable(event.model) as CrossModelRoot, + sourceClientId: event.sourceClientId + }) + ) + ); + this.toDisposeForSession.set(args.clientId, listenersForClient); + } + + protected disposeListeners(args: CloseModelArgs): void { + this.toDisposeForSession.get(args.clientId)?.forEach(disposable => disposable.dispose()); + this.toDisposeForSession.delete(args.clientId); } - protected async closeModel(uri: string): Promise { - await this.modelService.close(uri); + protected async closeModel(args: CloseModelArgs): Promise { + this.disposeListeners(args); + return this.modelService.close(args); } protected async requestModel(uri: string): Promise { @@ -103,13 +131,13 @@ export class ModelServer implements Disposable { return toSerializable(root) as CrossModelRoot; } - protected async updateModel(uri: string, model: CrossModelRoot): Promise { - const updated = await this.modelService.update(uri, model); + protected async updateModel(args: UpdateModelArgs): Promise { + const updated = await this.modelService.update(args); return toSerializable(updated) as CrossModelRoot; } - protected async saveModel(uri: string, model: AstNode): Promise { - await this.modelService.save(uri, model); + protected async saveModel(args: SaveModelArgs): Promise { + await this.modelService.save(args); } dispose(): void { diff --git a/extensions/crossmodel-lang/src/model-server/model-service.ts b/extensions/crossmodel-lang/src/model-server/model-service.ts index 935598fd..86e36591 100644 --- a/extensions/crossmodel-lang/src/model-server/model-service.ts +++ b/extensions/crossmodel-lang/src/model-server/model-service.ts @@ -2,11 +2,12 @@ * Copyright (c) 2023 CrossBreeze. ********************************************************************************/ +import { CloseModelArgs, ModelSavedEvent, ModelUpdatedEvent, OpenModelArgs, SaveModelArgs, UpdateModelArgs } from '@crossbreeze/protocol'; import { AstNode, DocumentState, isAstNode } from 'langium'; -import { Disposable } from 'vscode-languageserver'; -import { OptionalVersionedTextDocumentIdentifier, Range, TextDocumentEdit, TextEdit } from 'vscode-languageserver-protocol'; +import { Disposable, OptionalVersionedTextDocumentIdentifier, Range, TextDocumentEdit, TextEdit, uinteger } from 'vscode-languageserver'; import { URI } from 'vscode-uri'; import { CrossModelSharedServices } from '../language-server/cross-model-module'; +import { LANGUAGE_CLIENT_ID } from './openable-text-documents'; /** * The model service serves as a facade to access and update semantic models from the language server as a non-LSP client. @@ -18,15 +19,44 @@ export class ModelService { protected documentManager = shared.workspace.TextDocumentManager, protected documents = shared.workspace.LangiumDocuments, protected documentBuilder = shared.workspace.DocumentBuilder - ) {} + ) { + // sync updates with language client + this.documentBuilder.onBuildPhase(DocumentState.Validated, (allChangedDocuments, _token) => { + for (const changedDocument of allChangedDocuments) { + const sourceClientId = this.documentManager.getSourceClientId(changedDocument, allChangedDocuments); + if (sourceClientId === LANGUAGE_CLIENT_ID) { + continue; + } + const textDocument = changedDocument.textDocument; + if (this.documentManager.isOpenInLanguageClient(textDocument.uri)) { + // we only want to apply a text edit if the editor is already open + // because opening and updating at the same time might cause problems as the open call resets the document to filesystem + this.shared.lsp.Connection?.workspace.applyEdit({ + label: 'Update Model', + documentChanges: [ + // we use a null version to indicate that the version is known + // eslint-disable-next-line no-null/no-null + TextDocumentEdit.create(OptionalVersionedTextDocumentIdentifier.create(textDocument.uri, null), [ + TextEdit.replace(Range.create(0, 0, uinteger.MAX_VALUE, uinteger.MAX_VALUE), textDocument.getText()) + ]) + ] + }); + } + } + }); + } /** * Opens the document with the given URI for modification. * * @param uri document URI */ - async open(uri: string): Promise { - return this.documentManager.open(uri); + async open(args: OpenModelArgs): Promise { + return this.documentManager.open(args); + } + + isOpen(uri: string): boolean { + return this.documentManager.isOpen(uri); } /** @@ -34,8 +64,8 @@ export class ModelService { * * @param uri document URI */ - async close(uri: string): Promise { - return this.documentManager.close(uri); + async close(args: CloseModelArgs): Promise { + return this.documentManager.close(args); } /** @@ -54,7 +84,6 @@ export class ModelService { */ request(uri: string, guard: (item: unknown) => item is T): Promise; async request(uri: string, guard?: (item: unknown) => item is T): Promise { - this.open(uri); const document = this.documents.getOrCreateDocument(URI.parse(uri)); const root = document.parseResult.value; const check = guard ?? isAstNode; @@ -70,50 +99,27 @@ export class ModelService { * @param model semantic model or textual representation of it * @returns the stored semantic model */ - async update(uri: string, model: T | string): Promise { - await this.open(uri); - const document = this.documents.getOrCreateDocument(URI.parse(uri)); + async update(args: UpdateModelArgs): Promise { + await this.open(args); + const document = this.documents.getOrCreateDocument(URI.parse(args.uri)); const root = document.parseResult.value; if (!isAstNode(root)) { - throw new Error(`No AST node to update exists in '${uri}'`); + throw new Error(`No AST node to update exists in '${args.uri}'`); } const textDocument = document.textDocument; - const text = typeof model === 'string' ? model : this.serialize(URI.parse(uri), model); + const text = typeof args.model === 'string' ? args.model : this.serialize(URI.parse(args.uri), args.model); if (text === textDocument.getText()) { return document.parseResult.value as T; } - - if (this.documentManager.isOpenInTextEditor(uri)) { - // we only want to apply a text edit if the editor is already open - // because opening and updating at the same time might cause problems as the open call resets the document to filesystem - await this.shared.lsp.Connection?.workspace.applyEdit({ - label: 'Update Model', - documentChanges: [ - // we use a null version to indicate that the version is known - // eslint-disable-next-line no-null/no-null - TextDocumentEdit.create(OptionalVersionedTextDocumentIdentifier.create(textDocument.uri, null), [ - TextEdit.replace(Range.create(0, 0, textDocument.lineCount, textDocument.getText().length), text) - ]) - ] - }); - } - - await this.documentManager.update(uri, textDocument.version + 1, text); - await this.documentBuilder.update([URI.parse(uri)], []); - + await this.documentManager.update(args.uri, textDocument.version + 1, text, args.clientId); return document.parseResult.value as T; } - onUpdate(uri: string, listener: (model: T) => void): Disposable { - return this.documentBuilder.onBuildPhase(DocumentState.Validated, (allChangedDocuments, _token) => { - const changedDocument = allChangedDocuments.find(document => document.uri.toString() === uri); - if (changedDocument) { - listener(changedDocument.parseResult.value as T); - } - }); + onUpdate(uri: string, listener: (model: ModelUpdatedEvent) => void): Disposable { + return this.documentManager.onUpdate(uri, listener); } - onSave(uri: string, listener: (model: T) => void): Disposable { + onSave(uri: string, listener: (model: ModelSavedEvent) => void): Disposable { return this.documentManager.onSave(uri, listener); } @@ -123,14 +129,14 @@ export class ModelService { * @param uri document uri * @param model semantic model or text */ - async save(uri: string, model: AstNode | string): Promise { + async save(args: SaveModelArgs): Promise { // sync: implicit update of internal data structure to match file system (similar to workspace initialization) - if (this.documents.hasDocument(URI.parse(uri))) { - await this.update(uri, model); + if (this.documents.hasDocument(URI.parse(args.uri))) { + await this.update(args); } - const text = typeof model === 'string' ? model : this.serialize(URI.parse(uri), model); - return this.documentManager.save(uri, text); + const text = typeof args.model === 'string' ? args.model : this.serialize(URI.parse(args.uri), args.model); + return this.documentManager.save(args.uri, text, args.clientId); } /** diff --git a/extensions/crossmodel-lang/src/model-server/open-text-document-manager.ts b/extensions/crossmodel-lang/src/model-server/open-text-document-manager.ts index df7a2056..96e15f50 100644 --- a/extensions/crossmodel-lang/src/model-server/open-text-document-manager.ts +++ b/extensions/crossmodel-lang/src/model-server/open-text-document-manager.ts @@ -2,12 +2,22 @@ * Copyright (c) 2023 CrossBreeze. ********************************************************************************/ +import { CloseModelArgs, ModelSavedEvent, ModelUpdatedEvent, OpenModelArgs } from '@crossbreeze/protocol'; import * as fs from 'fs'; -import { AstNode, FileSystemProvider, LangiumDefaultSharedServices, LangiumDocuments } from 'langium'; +import { + AstNode, + DocumentBuilder, + DocumentState, + FileSystemProvider, + LangiumDefaultSharedServices, + LangiumDocument, + LangiumDocuments +} from 'langium'; import { Disposable } from 'vscode-languageserver'; import { TextDocumentIdentifier, TextDocumentItem, VersionedTextDocumentIdentifier } from 'vscode-languageserver-protocol'; import { TextDocument } from 'vscode-languageserver-textdocument'; import { URI } from 'vscode-uri'; +import { CrossModelLanguageMetaData } from '../language-server/generated/module'; import { AddedSharedModelServices } from './model-module'; import { OpenableTextDocuments } from './openable-text-documents'; @@ -20,17 +30,18 @@ export class OpenTextDocumentManager { protected textDocuments: OpenableTextDocuments; protected fileSystemProvider: FileSystemProvider; protected langiumDocs: LangiumDocuments; - - /** Normalized URIs of open documents */ - protected openDocuments: string[] = []; + protected documentBuilder: DocumentBuilder; constructor(services: AddedSharedModelServices & LangiumDefaultSharedServices) { this.textDocuments = services.workspace.TextDocuments; this.fileSystemProvider = services.workspace.FileSystemProvider; this.langiumDocs = services.workspace.LangiumDocuments; + this.documentBuilder = services.workspace.DocumentBuilder; - this.textDocuments.onDidOpen(event => this.open(event.document.uri, event.document.languageId)); - this.textDocuments.onDidClose(event => this.close(event.document.uri)); + this.textDocuments.onDidOpen(event => + this.open({ clientId: event.clientId, uri: event.document.uri, languageId: event.document.languageId }) + ); + this.textDocuments.onDidClose(event => this.close({ clientId: event.clientId, uri: event.document.uri })); } /** @@ -41,67 +52,94 @@ export class OpenTextDocumentManager { * @param listener Callback to be called * @returns Disposable object */ - onSave(uri: string, listener: (model: T) => void): Disposable { - return this.textDocuments.onDidSave(e => { - const documentURI = URI.parse(e.document.uri); + onSave(uri: string, listener: (model: ModelSavedEvent) => void): Disposable { + return this.textDocuments.onDidSave(event => { + const documentURI = URI.parse(event.document.uri); // Check if the uri of the saved document and the uri of the listener are equal. - if (e.document.uri === uri && documentURI !== undefined && this.langiumDocs.hasDocument(documentURI)) { + if (event.document.uri === uri && documentURI !== undefined && this.langiumDocs.hasDocument(documentURI)) { const document = this.langiumDocs.getOrCreateDocument(documentURI); const root = document.parseResult.value; - return listener(root as T); + return listener({ model: root as T, uri: event.document.uri, sourceClientId: event.clientId }); } return undefined; }); } - async open(uri: string, languageId?: string): Promise { - if (this.isOpen(uri)) { - return; - } - this.openDocuments.push(this.normalizedUri(uri)); - const textDocument = await this.readFromFilesystem(uri, languageId); - this.textDocuments.notifyDidOpenTextDocument({ textDocument }, false); + onUpdate(uri: string, listener: (model: ModelUpdatedEvent) => void): Disposable { + return this.documentBuilder.onBuildPhase(DocumentState.Validated, (allChangedDocuments, _token) => { + const changedDocument = allChangedDocuments.find(document => document.uri.toString() === uri); + if (changedDocument) { + const sourceClientId = this.getSourceClientId(changedDocument, allChangedDocuments); + listener({ + model: changedDocument.parseResult.value as T, + sourceClientId, + uri: changedDocument.textDocument.uri + }); + } + }); } - async close(uri: string): Promise { - if (!this.isOpen(uri)) { - return; + getSourceClientId(preferred: LangiumDocument, rest: LangiumDocument[]): string { + const clientId = this.textDocuments.getChangeSource(preferred.textDocument.uri, preferred.textDocument.version); + if (clientId) { + return clientId; } - this.removeFromOpenedDocuments(uri); - this.textDocuments.notifyDidCloseTextDocument({ textDocument: TextDocumentIdentifier.create(uri) }); + return ( + rest + .map(document => this.textDocuments.getChangeSource(document.textDocument.uri, document.textDocument.version)) + .find(source => source !== undefined) || 'unknown' + ); } - async update(uri: string, version: number, text: string): Promise { + async open(args: OpenModelArgs): Promise { + // only create a dummy document if it is already open as we use the synced state anyway + const textDocument = this.isOpen(args.uri) + ? this.createDummyDocument(args.uri) + : await this.createDocumentFromFileSystem(args.uri, args.languageId); + this.textDocuments.notifyDidOpenTextDocument({ textDocument }, args.clientId); + } + + async close(args: CloseModelArgs): Promise { + this.textDocuments.notifyDidCloseTextDocument({ textDocument: TextDocumentIdentifier.create(args.uri) }, args.clientId); + } + + async update(uri: string, version: number, text: string, clientId: string): Promise { if (!this.isOpen(uri)) { throw new Error(`Document ${uri} hasn't been opened for updating yet`); } - this.textDocuments.notifyDidChangeTextDocument({ - textDocument: VersionedTextDocumentIdentifier.create(uri, version), - contentChanges: [{ text }] - }); + this.textDocuments.notifyDidChangeTextDocument( + { + textDocument: VersionedTextDocumentIdentifier.create(uri, version), + contentChanges: [{ text }] + }, + clientId + ); } - async save(uri: string, text: string): Promise { + async save(uri: string, text: string, clientId: string): Promise { const vscUri = URI.parse(uri); fs.writeFileSync(vscUri.fsPath, text); - this.textDocuments.notifyDidSaveTextDocument({ textDocument: TextDocumentIdentifier.create(uri), text }); + this.textDocuments.notifyDidSaveTextDocument({ textDocument: TextDocumentIdentifier.create(uri), text }, clientId); } isOpen(uri: string): boolean { - return this.openDocuments.includes(this.normalizedUri(uri)); + return !!this.textDocuments.get(this.normalizedUri(uri)) || !!this.textDocuments.get(uri); } - isOpenInTextEditor(uri: string): boolean { - return this.textDocuments.isOpenInTextEditor(this.normalizedUri(uri)); + isOpenInLanguageClient(uri: string): boolean { + return this.textDocuments.isOpenInLanguageClient(this.normalizedUri(uri)); } - protected removeFromOpenedDocuments(uri: string): void { - this.openDocuments.splice(this.openDocuments.indexOf(this.normalizedUri(uri))); + protected createDummyDocument(uri: string): TextDocumentItem { + return TextDocumentItem.create(this.normalizedUri(uri), CrossModelLanguageMetaData.languageId, 1, ''); } - protected async readFromFilesystem(uri: string, languageId = 'cross-model'): Promise { + protected async createDocumentFromFileSystem( + uri: string, + languageId: string = CrossModelLanguageMetaData.languageId + ): Promise { const vscUri = URI.parse(uri); const content = this.fileSystemProvider.readFileSync(vscUri); return TextDocumentItem.create(vscUri.toString(), languageId, 1, content.toString()); diff --git a/extensions/crossmodel-lang/src/model-server/openable-text-documents.ts b/extensions/crossmodel-lang/src/model-server/openable-text-documents.ts index 595b9057..bae68d1d 100644 --- a/extensions/crossmodel-lang/src/model-server/openable-text-documents.ts +++ b/extensions/crossmodel-lang/src/model-server/openable-text-documents.ts @@ -3,185 +3,248 @@ * Copyright (c) Microsoft Corporation and EclipseSource. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. * ------------------------------------------------------------------------------------------ */ +import { basename } from 'path'; import { - CancellationToken, - DidChangeTextDocumentParams, - DidCloseTextDocumentParams, - DidOpenTextDocumentParams, - DidSaveTextDocumentParams, - Disposable, - Emitter, - HandlerResult, - RequestHandler, - TextDocumentChangeEvent, - TextDocuments, - TextDocumentsConfiguration, - TextDocumentSyncKind, - TextDocumentWillSaveEvent, - TextEdit, - WillSaveTextDocumentParams + CancellationToken, + DidChangeTextDocumentParams, + DidCloseTextDocumentParams, + DidOpenTextDocumentParams, + DidSaveTextDocumentParams, + Disposable, + Emitter, + Event, + HandlerResult, + RequestHandler, + TextDocumentChangeEvent, + TextDocuments, + TextDocumentsConfiguration, + TextDocumentSyncKind, + TextDocumentWillSaveEvent, + TextEdit, + WillSaveTextDocumentParams } from 'vscode-languageserver'; import { TextDocument } from 'vscode-languageserver-textdocument'; +import { URI } from 'vscode-uri'; +import { ClientLogger } from '../language-server/cross-model-client-logger'; -const OPENED_IN_TEXT_EDITOR_PROP = '_openedInTextEditor'; +export const LANGUAGE_CLIENT_ID = 'language-client'; + +export interface ClientTextDocumentChangeEvent extends TextDocumentChangeEvent { + clientId: string; +} /** - * This subclass of `TextDocuments` is actually entirely equivalent to `TextDocuments` but opens up - * methods to be able to invoke events from within the language server (see json-server.ts). - * - * Otherwise the document will be read all the time from the disk - * langium/src/workspace/documents.ts:222 which relies on _syncedDocuments to be open - * vscode-languageserver/lib/common/textDocuments.js:119 + * This subclass of `TextDocuments` supports multiple clients to open the same document and sync their state. */ export class OpenableTextDocuments extends TextDocuments { - public constructor(protected configuration: TextDocumentsConfiguration) { - super(configuration); - } - - protected get __syncedDocuments(): Map { - return this['_syncedDocuments']; - } - - protected get __onDidChangeContent(): Emitter> { - return this['_onDidChangeContent']; - } - - protected get __onDidOpen(): Emitter> { - return this['_onDidOpen']; - } - - protected get __onDidClose(): Emitter> { - return this['_onDidClose']; - } - - protected get __onDidSave(): Emitter> { - return this['_onDidSave']; - } - - protected get __onWillSave(): Emitter> { - return this['_onWillSave']; - } - - protected get __willSaveWaitUntil(): RequestHandler, TextEdit[], void> | undefined { - return this['_willSaveWaitUntil']; - } - - public override listen(connection: any): Disposable { - (connection).__textDocumentSync = TextDocumentSyncKind.Incremental; - const disposables: Disposable[] = []; - disposables.push( - connection.onDidOpenTextDocument((event: DidOpenTextDocumentParams) => { - this.notifyDidOpenTextDocument(event); - }) - ); - disposables.push( - connection.onDidChangeTextDocument((event: DidChangeTextDocumentParams) => { - this.notifyDidChangeTextDocument(event); - }) - ); - disposables.push( - connection.onDidCloseTextDocument((event: DidCloseTextDocumentParams) => { - this.notifyDidCloseTextDocument(event); - }) - ); - disposables.push( - connection.onWillSaveTextDocument((event: WillSaveTextDocumentParams) => { - this.notifyWillSaveTextDocument(event); - }) - ); - disposables.push( - connection.onWillSaveTextDocumentWaitUntil((event: WillSaveTextDocumentParams, token: CancellationToken) => - this.notifyWillSaveTextDocumentWaitUntil(event, token) - ) - ); - disposables.push( - connection.onDidSaveTextDocument((event: DidSaveTextDocumentParams) => { - this.notifyDidSaveTextDocument(event); - }) - ); - return Disposable.create(() => { - disposables.forEach(disposable => disposable.dispose()); - }); - } - - public notifyDidChangeTextDocument(event: DidChangeTextDocumentParams): void { - const td = event.textDocument; - const changes = event.contentChanges; - if (changes.length === 0) { - return; - } - - const { version } = td; - // eslint-disable-next-line no-null/no-null - if (version === null || version === undefined) { - throw new Error(`Received document change event for ${td.uri} without valid version identifier`); - } - - let syncedDocument = this.__syncedDocuments.get(td.uri); - if (syncedDocument !== undefined) { - if (syncedDocument.version >= td.version) { - console.log(`Skip update of document ${td.uri} as local version is newer (${syncedDocument.version} >= ${td.version})`); + protected __clientDocuments = new Map>(); + protected __changeHistory = new Map(); + + public constructor(protected configuration: TextDocumentsConfiguration, protected logger: ClientLogger) { + super(configuration); + } + + protected get __syncedDocuments(): Map { + return this['_syncedDocuments']; + } + + protected get __onDidChangeContent(): Emitter> { + return this['_onDidChangeContent']; + } + + override get onDidChangeContent(): Event> { + return this.__onDidChangeContent.event; + } + + protected get __onDidOpen(): Emitter> { + return this['_onDidOpen']; + } + + override get onDidOpen(): Event> { + return this.__onDidOpen.event; + } + + protected get __onDidClose(): Emitter> { + return this['_onDidClose']; + } + + override get onDidClose(): Event> { + return this.__onDidClose.event; + } + + protected get __onDidSave(): Emitter> { + return this['_onDidSave']; + } + + override get onDidSave(): Event> { + return this['__onDidSave'].event; + } + + protected get __onWillSave(): Emitter> { + return this['_onWillSave']; + } + + protected get __willSaveWaitUntil(): RequestHandler, TextEdit[], void> | undefined { + return this['_willSaveWaitUntil']; + } + + public override listen(connection: any): Disposable { + (connection).__textDocumentSync = TextDocumentSyncKind.Incremental; + const disposables: Disposable[] = []; + disposables.push( + connection.onDidOpenTextDocument((event: DidOpenTextDocumentParams) => { + this.notifyDidOpenTextDocument(event); + }) + ); + disposables.push( + connection.onDidChangeTextDocument((event: DidChangeTextDocumentParams) => { + this.notifyDidChangeTextDocument(event); + }) + ); + disposables.push( + connection.onDidCloseTextDocument((event: DidCloseTextDocumentParams) => { + this.notifyDidCloseTextDocument(event); + }) + ); + disposables.push( + connection.onWillSaveTextDocument((event: WillSaveTextDocumentParams) => { + this.notifyWillSaveTextDocument(event); + }) + ); + disposables.push( + connection.onWillSaveTextDocumentWaitUntil((event: WillSaveTextDocumentParams, token: CancellationToken) => + this.notifyWillSaveTextDocumentWaitUntil(event, token) + ) + ); + disposables.push( + connection.onDidSaveTextDocument((event: DidSaveTextDocumentParams) => { + this.notifyDidSaveTextDocument(event); + }) + ); + return Disposable.create(() => { + disposables.forEach(disposable => disposable.dispose()); + }); + } + + public notifyDidChangeTextDocument(event: DidChangeTextDocumentParams, clientId = LANGUAGE_CLIENT_ID): void { + const td = event.textDocument; + const changes = event.contentChanges; + if (changes.length === 0) { + return; + } + + const { version } = td; + // eslint-disable-next-line no-null/no-null + if (version === null || version === undefined) { + throw new Error(`Received document change event for ${td.uri} without valid version identifier`); + } + + let document = this.__syncedDocuments.get(td.uri); + if (document !== undefined) { + if (document.version >= td.version) { + this.log(document.uri, `Update is out of date (${document.version} >= ${td.version}): Ignore update by ${clientId}`); + return; + } + document = this.configuration.update(document, changes, version); + this.__syncedDocuments.set(td.uri, document); + const changeHistory = this.__changeHistory.get(td.uri) || []; + changeHistory[td.version] = clientId; + this.__changeHistory.set(td.uri, changeHistory); + this.log(document.uri, `Update to version ${td.version} by ${clientId}`); + this.__onDidChangeContent.fire(Object.freeze({ document, clientId })); + } + } + + public notifyDidCloseTextDocument(event: DidCloseTextDocumentParams, clientId = LANGUAGE_CLIENT_ID): void { + if (!this.isOpenInClient(event.textDocument.uri, clientId)) { + return; + } + this.__clientDocuments.get(event.textDocument.uri)?.delete(clientId); + const syncedDocument = this.__syncedDocuments.get(event.textDocument.uri); + if (syncedDocument !== undefined) { + this.log(syncedDocument.uri, `Closed synced document: ${syncedDocument.version} by ${clientId}`); + this.__onDidClose.fire(Object.freeze({ document: syncedDocument, clientId })); + + if (!this.__clientDocuments.get(event.textDocument.uri)?.size) { + // last client closed the document, delete sync state + this.log(syncedDocument.uri, `Remove synced document: ${syncedDocument.version} (no client left)`); + this.__syncedDocuments.delete(event.textDocument.uri); + this.__changeHistory.delete(event.textDocument.uri); + } + } + } + + public notifyWillSaveTextDocument(event: WillSaveTextDocumentParams): void { + const syncedDocument = this.__syncedDocuments.get(event.textDocument.uri); + if (syncedDocument !== undefined) { + this.__onWillSave.fire(Object.freeze({ document: syncedDocument, reason: event.reason })); + } + } + + public notifyWillSaveTextDocumentWaitUntil( + event: WillSaveTextDocumentParams, + token: CancellationToken + ): HandlerResult { + const syncedDocument = this.__syncedDocuments.get(event.textDocument.uri); + if (syncedDocument !== undefined && this.__willSaveWaitUntil) { + return this.__willSaveWaitUntil(Object.freeze({ document: syncedDocument, reason: event.reason }), token); + } else { + return []; + } + } + + public notifyDidSaveTextDocument(event: DidSaveTextDocumentParams, clientId = LANGUAGE_CLIENT_ID): void { + const syncedDocument = this.__syncedDocuments.get(event.textDocument.uri); + if (syncedDocument !== undefined) { + this.log(syncedDocument.uri, `Saved synced document: ${syncedDocument.version} by ${clientId}`); + this.__onDidSave.fire(Object.freeze({ document: syncedDocument, clientId })); + } + } + + public notifyDidOpenTextDocument(event: DidOpenTextDocumentParams, clientId = LANGUAGE_CLIENT_ID): void { + if (this.isOpenInClient(event.textDocument.uri, clientId)) { return; - } - syncedDocument = this.configuration.update(syncedDocument, changes, version); - this.__syncedDocuments.set(td.uri, syncedDocument); - this.__onDidChangeContent.fire(Object.freeze({ document: syncedDocument })); - } - } - - public notifyDidCloseTextDocument(event: DidCloseTextDocumentParams): void { - const syncedDocument = this.__syncedDocuments.get(event.textDocument.uri); - if (syncedDocument !== undefined) { - this.__syncedDocuments.delete(event.textDocument.uri); - this.__onDidClose.fire(Object.freeze({ document: syncedDocument })); - } - } - - public notifyWillSaveTextDocument(event: WillSaveTextDocumentParams): void { - const syncedDocument = this.__syncedDocuments.get(event.textDocument.uri); - if (syncedDocument !== undefined) { - this.__onWillSave.fire(Object.freeze({ document: syncedDocument, reason: event.reason })); - } - } - - public notifyWillSaveTextDocumentWaitUntil( - event: WillSaveTextDocumentParams, - token: CancellationToken - ): HandlerResult { - const syncedDocument = this.__syncedDocuments.get(event.textDocument.uri); - if (syncedDocument !== undefined && this.__willSaveWaitUntil) { - return this.__willSaveWaitUntil(Object.freeze({ document: syncedDocument, reason: event.reason }), token); - } else { - return []; - } - } - - public notifyDidSaveTextDocument(event: DidSaveTextDocumentParams): void { - const syncedDocument = this.__syncedDocuments.get(event.textDocument.uri); - if (syncedDocument !== undefined) { - this.__onDidSave.fire(Object.freeze({ document: syncedDocument })); - } - } - - public notifyDidOpenTextDocument(event: DidOpenTextDocumentParams, openedInTextEditor = true): void { - const td = event.textDocument; - const document = this.configuration.create(td.uri, td.languageId, td.version, td.text); - const wasOpenInTextEditor = this.isOpenInTextEditor(td.uri); - this.__syncedDocuments.set(td.uri, document); - this.markOpenInTextEditor(td.uri, wasOpenInTextEditor || openedInTextEditor); - const toFire = Object.freeze({ document }); - this.__onDidOpen.fire(toFire); - this.__onDidChangeContent.fire(toFire); - } - - isOpenInTextEditor(uri: string): boolean { - return !!(this.__syncedDocuments.get(uri) as any | undefined)?.[OPENED_IN_TEXT_EDITOR_PROP]; - } - - protected markOpenInTextEditor(uri: string, open: boolean): void { - const document = this.__syncedDocuments.get(uri); - if (document) { - (document as any)[OPENED_IN_TEXT_EDITOR_PROP] = open; - } - } + } + const td = event.textDocument; + let document = this.__syncedDocuments.get(td.uri); + const clients = this.__clientDocuments.get(td.uri) || new Set(); + clients.add(clientId); + this.__clientDocuments.set(td.uri, clients); + if (!document) { + // no synced document yet, create new one + this.log(td.uri, `Opened new document: ${td.version} by ${clientId}`); + document = this.configuration.create(td.uri, td.languageId, td.version, td.text); + this.__syncedDocuments.set(td.uri, document); + this.__changeHistory.set(td.uri, [clientId]); + const toFire = Object.freeze({ document, clientId }); + this.__onDidOpen.fire(toFire); + this.__onDidChangeContent.fire(toFire); + } else { + // document was already synced, so we just change a content change + this.log(td.uri, `Opened synced document: ${td.version} by ${clientId}`); + const toFire = Object.freeze({ document, clientId }); + this.__onDidChangeContent.fire(toFire); + } + } + + getChangeSource(uri: string, version: number): string | undefined { + return this.__changeHistory.get(uri)?.[version]; + } + + isOpen(uri: string): boolean { + return this.__syncedDocuments.has(uri); + } + + isOpenInClient(uri: string, client: string): boolean { + return !!this.__clientDocuments.get(uri)?.has(client); + } + + isOpenInLanguageClient(uri: string): boolean { + return this.isOpenInClient(uri, LANGUAGE_CLIENT_ID); + } + + protected log(uri: string, message: string): void { + const full = URI.parse(uri); + this.logger.info(`[Documents][${basename(full.fsPath)}] ${message}`); + } } diff --git a/packages/form-client/src/browser/form-editor-open-handler.ts b/packages/form-client/src/browser/form-editor-open-handler.ts index 0075dbd6..9e2e3704 100644 --- a/packages/form-client/src/browser/form-editor-open-handler.ts +++ b/packages/form-client/src/browser/form-editor-open-handler.ts @@ -3,24 +3,24 @@ ********************************************************************************/ import { MaybePromise, nls } from '@theia/core'; -import { NavigatableWidgetOpenHandler, WidgetOpenerOptions } from '@theia/core/lib/browser'; +import { NavigatableWidgetOpenHandler } from '@theia/core/lib/browser'; import URI from '@theia/core/lib/common/uri'; import { injectable } from '@theia/core/shared/inversify'; import { FormEditorWidget } from './form-editor-widget'; @injectable() export class FormEditorOpenHandler extends NavigatableWidgetOpenHandler { - static ID = 'form-editor-opener'; + static ID = 'form-editor-opener'; - readonly id = FormEditorOpenHandler.ID; // must match the id of the widget factory - readonly label = nls.localize('form-client/form-editor', 'Form Editor'); + readonly id = FormEditorOpenHandler.ID; // must match the id of the widget factory + readonly label = nls.localize('form-client/form-editor', 'Form Editor'); - canHandle(uri: URI, options?: WidgetOpenerOptions): MaybePromise { - return uri.path.ext === '.cm' ? 1 : -1; - } + canHandle(uri: URI): MaybePromise { + return uri.path.ext === '.cm' ? 1 : -1; + } } export function createFormEditorId(uri: URI, counter?: number): string { - // ensure we create a unique ID - return FormEditorOpenHandler.ID + `:${uri.toString()}` + (counter !== undefined ? `:${counter}` : ''); + // ensure we create a unique ID + return FormEditorOpenHandler.ID + `:${uri.toString()}` + (counter !== undefined ? `:${counter}` : ''); } diff --git a/packages/form-client/src/browser/form-editor-widget.tsx b/packages/form-client/src/browser/form-editor-widget.tsx index 4589690a..73f00424 100644 --- a/packages/form-client/src/browser/form-editor-widget.tsx +++ b/packages/form-client/src/browser/form-editor-widget.tsx @@ -5,7 +5,15 @@ import { ModelService, ModelServiceClient } from '@crossbreeze/model-service/lib/common'; import { CrossModelRoot } from '@crossbreeze/protocol'; import { CommandService, Emitter, Event } from '@theia/core'; -import { LabelProvider, NavigatableWidget, NavigatableWidgetOptions, ReactWidget, SaveOptions, Saveable } from '@theia/core/lib/browser'; +import { + LabelProvider, + Message, + NavigatableWidget, + NavigatableWidgetOptions, + ReactWidget, + SaveOptions, + Saveable +} from '@theia/core/lib/browser'; import URI from '@theia/core/lib/common/uri'; import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import * as React from '@theia/core/shared/react'; @@ -19,6 +27,8 @@ export interface FormEditorWidgetOptions extends NavigatableWidgetOptions { id: string; } +const FORM_CLIENT_ID = 'form-client'; + @injectable() export class FormEditorWidget extends ReactWidget implements NavigatableWidget, Saveable { dirty = false; @@ -47,9 +57,9 @@ export class FormEditorWidget extends ReactWidget implements NavigatableWidget, this.getResourceUri = this.getResourceUri.bind(this); this.loadModel(); - this.formClient.onUpdate(document => { - if (document.uri === this.getResourceUri().toString()) { - this.modelUpdated(document.model); + this.formClient.onUpdate(event => { + if (event.sourceClientId !== FORM_CLIENT_ID && event.uri === this.getResourceUri().toString()) { + this.modelUpdated(event.model); } }); } @@ -57,8 +67,7 @@ export class FormEditorWidget extends ReactWidget implements NavigatableWidget, protected async loadModel(): Promise { try { const uri = this.getResourceUri().toString(); - await this.modelService.open(uri); - const model = await this.modelService.request(uri); + const model = await this.modelService.open({ uri, clientId: FORM_CLIENT_ID }); if (model) { this.syncedModel = model; } @@ -75,13 +84,13 @@ export class FormEditorWidget extends ReactWidget implements NavigatableWidget, } this.setDirty(false); - await this.modelService.save(this.getResourceUri().toString(), this.syncedModel); + await this.modelService.save({ uri: this.getResourceUri().toString(), model: this.syncedModel, clientId: FORM_CLIENT_ID }); } protected updateModel = debounce((model: CrossModelRoot) => { if (!deepEqual(this.syncedModel, model)) { this.syncedModel = model; - this.modelService.update(this.getResourceUri().toString(), model); + this.modelService.update({ uri: this.getResourceUri().toString(), model, clientId: FORM_CLIENT_ID }); } }, 200); @@ -93,7 +102,7 @@ export class FormEditorWidget extends ReactWidget implements NavigatableWidget, } override close(): void { - this.modelService.close(this.getResourceUri().toString()); + this.modelService.close({ uri: this.getResourceUri().toString(), clientId: FORM_CLIENT_ID }); super.close(); } @@ -117,11 +126,26 @@ export class FormEditorWidget extends ReactWidget implements NavigatableWidget, return ; } + protected override onActivateRequest(msg: Message): void { + super.onActivateRequest(msg); + const focusInput = (): boolean => { + const inputs = this.node.getElementsByTagName('input'); + if (inputs.length > 0) { + inputs[0].focus(); + return true; + } + return false; + }; + if (!focusInput()) { + setTimeout(focusInput, 500); + } + } + getResourceUri(): URI { return new URI(this.options.uri); } createMoveToUri(resourceUri: URI): URI | undefined { - return undefined; + return resourceUri; } } diff --git a/packages/form-client/src/browser/react-components/entity-components/tabs/EntityGeneralTab.tsx b/packages/form-client/src/browser/react-components/entity-components/tabs/EntityGeneralTab.tsx index fe17b941..85ea6401 100644 --- a/packages/form-client/src/browser/react-components/entity-components/tabs/EntityGeneralTab.tsx +++ b/packages/form-client/src/browser/react-components/entity-components/tabs/EntityGeneralTab.tsx @@ -1,9 +1,9 @@ /******************************************************************************** * Copyright (c) 2023 CrossBreeze. ********************************************************************************/ +import { CrossModelRoot } from '@crossbreeze/protocol'; import * as React from '@theia/core/shared/react'; import { ModelContext, ModelDispatchContext, ModelReducer } from '../../ModelContext'; -import { CrossModelRoot } from '@crossbreeze/protocol'; interface GeneralTabProps {} @@ -32,7 +32,6 @@ export function GeneralTab(props: GeneralTabProps): React.ReactElement {
) => { diff --git a/packages/model-service/src/browser/model-service-client.ts b/packages/model-service/src/browser/model-service-client.ts index fafa7a1b..a1f545e4 100644 --- a/packages/model-service/src/browser/model-service-client.ts +++ b/packages/model-service/src/browser/model-service-client.ts @@ -2,26 +2,21 @@ * Copyright (c) 2023 CrossBreeze. ********************************************************************************/ +import { CrossModelRoot, ModelUpdatedEvent } from '@crossbreeze/protocol'; import { Emitter } from '@theia/core'; import { injectable } from '@theia/core/shared/inversify'; import { ModelServiceClient } from '../common/model-service-rpc'; -import { CrossModelRoot } from '@crossbreeze/protocol'; - -export interface ModelDocument { - uri: string; - model: CrossModelRoot; -} @injectable() export class ModelServiceClientImpl implements ModelServiceClient { - protected onUpdateEmitter = new Emitter(); + protected onUpdateEmitter = new Emitter>(); onUpdate = this.onUpdateEmitter.event; async getName(): Promise { return 'ModelServiceClient'; } - async updateModel(uri: string, model: CrossModelRoot): Promise { - this.onUpdateEmitter.fire({ uri, model }); + async updateModel(event: ModelUpdatedEvent): Promise { + this.onUpdateEmitter.fire(event); } } diff --git a/packages/model-service/src/common/model-service-rpc.ts b/packages/model-service/src/common/model-service-rpc.ts index abadd45c..4a4ec8a3 100644 --- a/packages/model-service/src/common/model-service-rpc.ts +++ b/packages/model-service/src/common/model-service-rpc.ts @@ -2,9 +2,16 @@ * Copyright (c) 2023 CrossBreeze. ********************************************************************************/ -import { CrossModelRoot, DiagramNodeEntity } from '@crossbreeze/protocol'; -import { JsonRpcServer, Event } from '@theia/core'; -import { ModelDocument } from '../browser'; +import { + CloseModelArgs, + CrossModelRoot, + DiagramNodeEntity, + ModelUpdatedEvent, + OpenModelArgs, + SaveModelArgs, + UpdateModelArgs +} from '@crossbreeze/protocol'; +import { Event, JsonRpcServer } from '@theia/core'; /** Path used to communicate between the Theia frontend and backend */ export const MODEL_SERVICE_PATH = '/services/model-service'; @@ -14,17 +21,17 @@ export const MODEL_SERVICE_PATH = '/services/model-service'; */ export const ModelService = Symbol('ModelService'); export interface ModelService extends JsonRpcServer { - open(uri: string): Promise; - close(uri: string): Promise; + open(args: OpenModelArgs): Promise; + close(args: CloseModelArgs): Promise; request(uri: string): Promise; requestDiagramNodeEntityModel(uri: string, id: string): Promise; - update(uri: string, model: CrossModelRoot): Promise; - save(uri: string, model: CrossModelRoot): Promise; + update(args: UpdateModelArgs): Promise; + save(args: SaveModelArgs): Promise; } export const ModelServiceClient = Symbol('ModelServiceClient'); export interface ModelServiceClient { getName(): Promise; - updateModel(uri: string, model: CrossModelRoot): Promise; - onUpdate: Event; + updateModel(args: ModelUpdatedEvent): Promise; + onUpdate: Event>; } diff --git a/packages/model-service/src/node/model-service.ts b/packages/model-service/src/node/model-service.ts index 8e81a65d..3a12f349 100644 --- a/packages/model-service/src/node/model-service.ts +++ b/packages/model-service/src/node/model-service.ts @@ -4,17 +4,21 @@ import { waitForTemporaryFileContent } from '@crossbreeze/core/lib/node'; import { CloseModel, + CloseModelArgs, CrossModelRoot, DiagramNodeEntity, MODELSERVER_PORT_FILE, OnSave, OnUpdated, OpenModel, + OpenModelArgs, PORT_FOLDER, RequestModel, RequestModelDiagramNode, SaveModel, - UpdateModel + SaveModelArgs, + UpdateModel, + UpdateModelArgs } from '@crossbreeze/protocol'; import { URI } from '@theia/core'; import { Deferred } from '@theia/core/lib/common/promise-util'; @@ -85,14 +89,14 @@ export class ModelServiceImpl implements ModelService, BackendApplicationContrib return Number.parseInt(port, 10); } - async open(uri: string): Promise { + async open(args: OpenModelArgs): Promise { await this.initializeServer(); - await this.connection.sendRequest(OpenModel, uri); + return this.connection.sendRequest(OpenModel, args); } - async close(uri: string): Promise { + async close(args: CloseModelArgs): Promise { await this.initializeServer(); - await this.connection.sendRequest(CloseModel, uri); + await this.connection.sendRequest(CloseModel, args); } async request(uri: string): Promise { @@ -100,14 +104,14 @@ export class ModelServiceImpl implements ModelService, BackendApplicationContrib return this.connection.sendRequest(RequestModel, uri); } - async update(uri: string, model: CrossModelRoot): Promise { + async update(args: UpdateModelArgs): Promise { await this.initializeServer(); - return this.connection.sendRequest(UpdateModel, uri, model); + return this.connection.sendRequest(UpdateModel, args); } - async save(uri: string, model: CrossModelRoot): Promise { + async save(args: SaveModelArgs): Promise { await this.initializeServer(); - return this.connection.sendRequest(SaveModel, uri, model); + return this.connection.sendRequest(SaveModel, args); } dispose(): void { @@ -123,11 +127,11 @@ export class ModelServiceImpl implements ModelService, BackendApplicationContrib } setUpListeners(): void { - this.connection.onNotification(OnSave, (uri, model) => { - this.client?.updateModel(uri, model); + this.connection.onNotification(OnSave, event => { + this.client?.updateModel(event); }); - this.connection.onNotification(OnUpdated, (uri, model) => { - this.client?.updateModel(uri, model); + this.connection.onNotification(OnUpdated, event => { + this.client?.updateModel(event); }); } diff --git a/packages/property-view/src/browser/model-data-service.ts b/packages/property-view/src/browser/model-data-service.ts new file mode 100644 index 00000000..c27b75d7 --- /dev/null +++ b/packages/property-view/src/browser/model-data-service.ts @@ -0,0 +1,67 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ + +import { ModelService } from '@crossbreeze/model-service/lib/common'; +import { CrossModelRoot, DiagramNodeEntity } from '@crossbreeze/protocol'; +import { GlspSelection } from '@eclipse-glsp/theia-integration'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { PropertyDataService } from '@theia/property-view/lib/browser/property-data-service'; + +export const PROPERTY_CLIENT_ID = 'property-view-client'; + +@injectable() +export class ModelDataService implements PropertyDataService { + id = 'model-property-data-service'; + label = 'ModelPropertyDataService'; + currentUri?: string; + + @inject(ModelService) protected modelService: ModelService; + + canHandleSelection(selection: GlspSelection | undefined): number { + const canHandle = GlspSelection.is(selection) ? 1 : 0; + + // Close the previous file if there is a new selection the property view can not handle + if (canHandle === 0 && this.currentUri) { + this.modelService.close({ uri: this.currentUri, clientId: PROPERTY_CLIENT_ID }); + } + + return canHandle; + } + + protected async closeCurrentModel(): Promise { + if (this.currentUri) { + return this.modelService.close({ uri: this.currentUri, clientId: PROPERTY_CLIENT_ID }); + } + this.currentUri = undefined; + } + + protected async openCurrentModel(): Promise { + if (this.currentUri) { + return this.modelService.open({ uri: this.currentUri, clientId: PROPERTY_CLIENT_ID }); + } + return undefined; + } + + protected async getSelectedEntity(selection: GlspSelection | undefined): Promise { + if (selection && GlspSelection.is(selection) && selection.sourceUri && selection.selectedElementsIDs.length !== 0) { + return this.modelService.requestDiagramNodeEntityModel(selection.sourceUri, selection.selectedElementsIDs[0]); + } + return undefined; + } + + async providePropertyData(selection: GlspSelection | undefined): Promise { + const entity = await this.getSelectedEntity(selection); + if (!entity) { + this.closeCurrentModel(); + return undefined; + } + const newUri = entity.uri; + if (newUri !== this.currentUri) { + await this.closeCurrentModel(); + } + this.currentUri = newUri; + await this.openCurrentModel(); + return entity; + } +} diff --git a/packages/property-view/src/browser/model-property-widget.tsx b/packages/property-view/src/browser/model-property-widget.tsx index 93cd38f5..e68a7b75 100644 --- a/packages/property-view/src/browser/model-property-widget.tsx +++ b/packages/property-view/src/browser/model-property-widget.tsx @@ -13,6 +13,7 @@ import { IActionDispatcher } from '@eclipse-glsp/client'; import { GLSPDiagramWidget, GlspSelection } from '@eclipse-glsp/theia-integration'; import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell'; import { inject, injectable } from '@theia/core/shared/inversify'; +import { PROPERTY_CLIENT_ID } from './model-data-service'; import { App } from './react-components/App'; @injectable() @@ -60,7 +61,7 @@ export class ModelPropertyWidget extends ReactWidget implements PropertyViewCont if (this.model === undefined || this.uri === undefined) { throw new Error('Cannot save undefined model'); } - this.modelService.update(this.uri, this.model); + this.modelService.update({ uri: this.uri, model: this.model, clientId: PROPERTY_CLIENT_ID }); } protected async updateModel(model: CrossModelRoot): Promise { diff --git a/packages/property-view/src/browser/property-view-frontend-module.ts b/packages/property-view/src/browser/property-view-frontend-module.ts index ee77278c..6923feb5 100644 --- a/packages/property-view/src/browser/property-view-frontend-module.ts +++ b/packages/property-view/src/browser/property-view-frontend-module.ts @@ -3,12 +3,12 @@ ********************************************************************************/ import { ContainerModule } from '@theia/core/shared/inversify'; -import { PropertyViewWidgetProvider } from '@theia/property-view/lib/browser/property-view-widget-provider'; -import { ModelPropertyWidgetProvider } from './model-property-widget-provider'; import { PropertyDataService } from '@theia/property-view/lib/browser/property-data-service'; -import { ModelDataService } from '../common/model-data-service'; +import { PropertyViewWidgetProvider } from '@theia/property-view/lib/browser/property-view-widget-provider'; import '../../src/style/property-view.css'; +import { ModelDataService } from './model-data-service'; import { ModelPropertyWidget } from './model-property-widget'; +import { ModelPropertyWidgetProvider } from './model-property-widget-provider'; export default new ContainerModule((bind, _unbind, _isBound, rebind) => { // To make the property widget working diff --git a/packages/property-view/src/common/model-data-service.ts b/packages/property-view/src/common/model-data-service.ts deleted file mode 100644 index 89173aca..00000000 --- a/packages/property-view/src/common/model-data-service.ts +++ /dev/null @@ -1,49 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2023 CrossBreeze. - ********************************************************************************/ - -import { ModelService } from '@crossbreeze/model-service/lib/common'; -import { DiagramNodeEntity } from '@crossbreeze/protocol'; -import { GlspSelection } from '@eclipse-glsp/theia-integration'; -import { inject, injectable } from '@theia/core/shared/inversify'; -import { PropertyDataService } from '@theia/property-view/lib/browser/property-data-service'; -@injectable() -export class ModelDataService implements PropertyDataService { - id = 'model-property-data-service'; - label = 'ModelPropertyDataService'; - currentUri: string; - - @inject(ModelService) protected modelService: ModelService; - - canHandleSelection(selection: GlspSelection | undefined): number { - const canHandle = GlspSelection.is(selection) ? 1 : 0; - - // Close the previous file if there is a new selection the property view can not handle - if (canHandle === 0 && this.currentUri !== '') { - this.modelService.close(this.currentUri); - } - - return canHandle; - } - - async providePropertyData(selection: GlspSelection | undefined): Promise { - if (selection && GlspSelection.is(selection) && selection.sourceUri && selection.selectedElementsIDs.length !== 0) { - const entity: DiagramNodeEntity | undefined = await this.modelService.requestDiagramNodeEntityModel( - selection.sourceUri, - selection.selectedElementsIDs[0] - ); - - if (entity) { - if (this.currentUri && this.currentUri !== entity.uri) { - await this.modelService.close(this.currentUri); - this.currentUri = entity.uri; - await this.modelService.open(this.currentUri); - } - - return entity; - } - } - - return undefined; - } -} diff --git a/packages/protocol/src/model-service/protocol.ts b/packages/protocol/src/model-service/protocol.ts index 3ce56a2d..c2f4b6af 100644 --- a/packages/protocol/src/model-service/protocol.ts +++ b/packages/protocol/src/model-service/protocol.ts @@ -52,13 +52,44 @@ export function isDiagramNodeEntity(model?: any): model is DiagramNodeEntity { return !!model && model.uri && model.model && isCrossModelRoot(model.model); } -export const OpenModel = new rpc.RequestType1('server/open'); -export const CloseModel = new rpc.RequestType1('server/close'); +export interface ClientModelArgs { + uri: string; + clientId: string; +} + +export interface OpenModelArgs extends ClientModelArgs { + languageId?: string; +} + +export interface CloseModelArgs extends ClientModelArgs {} + +export interface UpdateModelArgs extends ClientModelArgs { + model: T | string; +} + +export interface SaveModelArgs extends ClientModelArgs { + model: T | string; +} + +export interface ModelUpdatedEvent { + uri: string; + model: T; + sourceClientId: string; +} + +export interface ModelSavedEvent { + uri: string; + model: T; + sourceClientId: string; +} + +export const OpenModel = new rpc.RequestType1('server/open'); +export const CloseModel = new rpc.RequestType1('server/close'); export const RequestModel = new rpc.RequestType1('server/request'); export const RequestModelDiagramNode = new rpc.RequestType2( 'server/requestModelDiagramNode' ); -export const UpdateModel = new rpc.RequestType2('server/update'); -export const SaveModel = new rpc.RequestType2('server/save'); -export const OnSave = new rpc.NotificationType2('server/onSave'); -export const OnUpdated = new rpc.NotificationType2('server/onUpdated'); +export const UpdateModel = new rpc.RequestType1, CrossModelRoot, void>('server/update'); +export const SaveModel = new rpc.RequestType1, void, void>('server/save'); +export const OnSave = new rpc.NotificationType1>('server/onSave'); +export const OnUpdated = new rpc.NotificationType1>('server/onUpdated'); From 963cf24b0af831b57dd5d779ac6739b41768907d Mon Sep 17 00:00:00 2001 From: Harmen Wessels Date: Wed, 18 Oct 2023 12:52:35 +0000 Subject: [PATCH 16/22] Fixed wrong use of name vs name_val when showing and synching the model. --- .../src/glsp-server/model/builders/node-builder.ts | 2 +- packages/model-service/src/node/model-service.ts | 1 + .../src/browser/react-components/ModelContext.tsx | 4 ++-- .../react-components/views/EntityPropertyAttributeGrid.tsx | 2 +- .../src/browser/react-components/views/EntityPropertyView.tsx | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/extensions/crossmodel-lang/src/glsp-server/model/builders/node-builder.ts b/extensions/crossmodel-lang/src/glsp-server/model/builders/node-builder.ts index 828b2b4f..c6b7d957 100644 --- a/extensions/crossmodel-lang/src/glsp-server/model/builders/node-builder.ts +++ b/extensions/crossmodel-lang/src/glsp-server/model/builders/node-builder.ts @@ -33,7 +33,7 @@ export class GEntityNodeBuilder extends GNodeBuilder { .addCssClass('entity-header-compartment') .add( GLabel.builder() - .text(entityRef?.name || 'unresolved') + .text(entityRef?.name_val || 'unresolved') .id(`${this.proxy.id}_label`) .addCssClass('entity-header-label') .build() diff --git a/packages/model-service/src/node/model-service.ts b/packages/model-service/src/node/model-service.ts index 3a12f349..4c575317 100644 --- a/packages/model-service/src/node/model-service.ts +++ b/packages/model-service/src/node/model-service.ts @@ -69,6 +69,7 @@ export class ModelServiceImpl implements ModelService, BackendApplicationContrib this.connection.onClose(() => connected.reject('No connection to ModelServer.')); socket.on('close', () => connected.reject('No connection to ModelServer')); socket.on('ready', () => connected.resolve()); + socket.on('error', error => console.error('Error was thrown on the ModelServer connection: %s; %s', error.name, error.message)); socket.connect({ port }); this.connection.listen(); diff --git a/packages/property-view/src/browser/react-components/ModelContext.tsx b/packages/property-view/src/browser/react-components/ModelContext.tsx index 41ff0b68..db7df2a9 100644 --- a/packages/property-view/src/browser/react-components/ModelContext.tsx +++ b/packages/property-view/src/browser/react-components/ModelContext.tsx @@ -65,7 +65,7 @@ export function ModelReducer(model: CrossModelRoot, action: any): CrossModelRoot throw Error('action.name undefined'); } - model.entity.name = action.name; + model.entity.name_val = action.name; return model; @@ -100,7 +100,7 @@ export function ModelReducer(model: CrossModelRoot, action: any): CrossModelRoot throw Error('action.id or name is undefined'); } - model.entity.attributes[action.id].name = action.name; + model.entity.attributes[action.id].name_val = action.name; return model; diff --git a/packages/property-view/src/browser/react-components/views/EntityPropertyAttributeGrid.tsx b/packages/property-view/src/browser/react-components/views/EntityPropertyAttributeGrid.tsx index d1ea3ed9..71c532c1 100644 --- a/packages/property-view/src/browser/react-components/views/EntityPropertyAttributeGrid.tsx +++ b/packages/property-view/src/browser/react-components/views/EntityPropertyAttributeGrid.tsx @@ -236,7 +236,7 @@ function CustomSelect(props: any): React.ReactElement { function createRows(attributes: Array): GridRowsProp { const rows = attributes.map((attribute, index) => ({ id: index, - name: attribute.name, + name: attribute.name_val, value: attribute.datatype })); diff --git a/packages/property-view/src/browser/react-components/views/EntityPropertyView.tsx b/packages/property-view/src/browser/react-components/views/EntityPropertyView.tsx index e9dc15c6..d8879fa7 100644 --- a/packages/property-view/src/browser/react-components/views/EntityPropertyView.tsx +++ b/packages/property-view/src/browser/react-components/views/EntityPropertyView.tsx @@ -56,7 +56,7 @@ function EntityPropertyGeneral(props: EntityPropertyGeneralProps): React.ReactEl ) => { dispatch({ type: 'entity:change-name', name: e.target.value ? e.target.value : '' }); }} From c9d709bde17cc3bd8a8e5f87b5335323e08dfa04 Mon Sep 17 00:00:00 2001 From: Harmen Wessels Date: Wed, 18 Oct 2023 12:52:58 +0000 Subject: [PATCH 17/22] Added missing name property on Order entity. --- examples/yaml-example/entities/voorbeeld_taal_entity_order.cm | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/yaml-example/entities/voorbeeld_taal_entity_order.cm b/examples/yaml-example/entities/voorbeeld_taal_entity_order.cm index 5fa73c2b..4791d007 100644 --- a/examples/yaml-example/entities/voorbeeld_taal_entity_order.cm +++ b/examples/yaml-example/entities/voorbeeld_taal_entity_order.cm @@ -1,5 +1,6 @@ entity: id: "Order" + name: "Order" description: "Order placed by a customer in the Customer table." attributes: - id: "Id" From 6c51f51a25a0b91d39435bf2612e002166706286 Mon Sep 17 00:00:00 2001 From: Harmen Wessels Date: Wed, 18 Oct 2023 12:53:37 +0000 Subject: [PATCH 18/22] Initial gitpod setup. --- .gitpod.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .gitpod.yml diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 00000000..069b9077 --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,17 @@ +# This configuration file was automatically generated by Gitpod. +# Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml) +# and commit this file to your remote git repository to share the goodness with others. + +# Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart + +# To have the dependencies for Theia installed execute the following commands in the GitPod workspace once: +# sudo apt-get install libsecret-1-dev libxkbfile-dev +# nvm install 16.20.0 +# pyenv install -s 3.11.4 +# pyenv global 3.11.4 + +tasks: + - init: yarn install && yarn run build + command: yarn run watch + + From d44a1f067e3af0d91425378f1833185662bdf169 Mon Sep 17 00:00:00 2001 From: Harmen Wessels <97173058+harmen-xb@users.noreply.github.com> Date: Wed, 18 Oct 2023 15:37:27 +0200 Subject: [PATCH 19/22] Added yarn test in build action. --- .github/actions/build/action.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index 0f6dd5f4..47ee24f3 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -32,24 +32,27 @@ runs: # windows-2019 # is needed because the Setup Python task does not work wit - name: Set up yarn to use version 1.22.19 uses: Amadevus/pwsh-script@v2.0.3 with: - # PowerShell script to execute in Actions-hydrated context script: | yarn policies set-version 1.22.19 - name: Yarn version uses: Amadevus/pwsh-script@v2.0.3 with: - # PowerShell script to execute in Actions-hydrated context script: | yarn --version | Write-Host - name: Yarn build uses: Amadevus/pwsh-script@v2.0.3 with: - # PowerShell script to execute in Actions-hydrated context script: | yarn + - name: Yarn test + uses: Amadevus/pwsh-script@v2.0.3 + with: + script: | + yarn test + #- name: Yarn start browser # run: | # yarn start:browser From 5ae77b2730afd73d8005f76d788b37d3500d3bea Mon Sep 17 00:00:00 2001 From: Harmen Wessels <97173058+harmen-xb@users.noreply.github.com> Date: Wed, 18 Oct 2023 15:47:45 +0200 Subject: [PATCH 20/22] Refactored test build part into seperate action and added in feature build only (for now). --- .github/actions/test/action.yml | 11 +++++++++++ .github/workflows/feature.yml | 18 ++++++++++-------- 2 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 .github/actions/test/action.yml diff --git a/.github/actions/test/action.yml b/.github/actions/test/action.yml new file mode 100644 index 00000000..9809a87f --- /dev/null +++ b/.github/actions/test/action.yml @@ -0,0 +1,11 @@ +name: test +description: 'Test CrossModel' + +runs: # windows-2019 # is needed because the Setup Python task does not work with ubuntu + using: 'composite' + steps: + - name: Yarn test + uses: Amadevus/pwsh-script@v2.0.3 + with: + script: | + yarn test diff --git a/.github/workflows/feature.yml b/.github/workflows/feature.yml index efca7754..810eacfc 100644 --- a/.github/workflows/feature.yml +++ b/.github/workflows/feature.yml @@ -1,16 +1,18 @@ name: feature on: - push: - branches: - - feature/* + push: + branches: + - feature/* jobs: build: runs-on: windows-2019 # is needed because the Setup Python task does not work with ubuntu steps: - # Checkout the code. - - name: check out # is needed to read the files on the root folder of the repo - uses: actions/checkout@v3 - # Use the build-addin action. - - uses: ./.github/actions/build + # Checkout the code. + - name: check out # is needed to read the files on the root folder of the repo + uses: actions/checkout@v3 + # Use the build action. + - uses: ./.github/actions/build + # Use the test action. + - uses: ./.github/actions/test From 6a39bd403a61d99cedcee4ec1177a47b5cd206a0 Mon Sep 17 00:00:00 2001 From: Harmen Wessels <97173058+harmen-xb@users.noreply.github.com> Date: Wed, 18 Oct 2023 17:12:27 +0200 Subject: [PATCH 21/22] Disabled test action for now, since it doesn't seem to work on GitHub easily. --- .github/workflows/feature.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/feature.yml b/.github/workflows/feature.yml index 810eacfc..d91fd193 100644 --- a/.github/workflows/feature.yml +++ b/.github/workflows/feature.yml @@ -15,4 +15,4 @@ jobs: # Use the build action. - uses: ./.github/actions/build # Use the test action. - - uses: ./.github/actions/test + # - uses: ./.github/actions/test From 734816edc09601cfdaf098f7063d614fa30f0cf9 Mon Sep 17 00:00:00 2001 From: Harmen Wessels <97173058+harmen-xb@users.noreply.github.com> Date: Wed, 18 Oct 2023 22:53:39 +0200 Subject: [PATCH 22/22] Refactored serializer so indentation and characters used are in constants. Updated/added some comments. Updated model-service-impl so have more specific console/promise messages and added some comments. --- .../language-server/cross-model-serializer.ts | 24 ++++-- .../src/model-server/launch.ts | 82 +++++++++---------- .../model-service/src/node/model-service.ts | 18 +++- 3 files changed, 72 insertions(+), 52 deletions(-) diff --git a/extensions/crossmodel-lang/src/language-server/cross-model-serializer.ts b/extensions/crossmodel-lang/src/language-server/cross-model-serializer.ts index 642f7ce7..edb55c78 100644 --- a/extensions/crossmodel-lang/src/language-server/cross-model-serializer.ts +++ b/extensions/crossmodel-lang/src/language-server/cross-model-serializer.ts @@ -35,25 +35,34 @@ const PROPERTY_ORDER = [ * cf. https://github.com/langium/langium/discussions/863 */ export class CrossModelSerializer implements Serializer { + // New line character. + static readonly CHAR_NEWLINE = '\n'; + // Indentation character. + static readonly CHAR_INDENTATION = ' '; + // The amount of spaces to use to indent an object. + static readonly INDENTATION_AMOUNT_OBJECT = 4; + // The amount of spaces to use to indent an array. + static readonly INDENTATION_AMOUNT_ARRAY = 2; + constructor(protected services: CrossModelServices, protected refNameProvider = services.references.QualifiedNameProvider) {} serialize(root: CrossModelRoot): string { const newRoot: CrossModelRoot | Entity | Relationship | SystemDiagram = this.toSerializableObject(root); - return this.serializeValue(newRoot, -4); + return this.serializeValue(newRoot, CrossModelSerializer.INDENTATION_AMOUNT_OBJECT * -1); } private serializeValue(value: any, indentationLevel: number): string { if (Array.isArray(value)) { return this.serializeArray(value, indentationLevel); } else if (typeof value === 'object' && value !== undefined) { - return this.serializeObject(value, indentationLevel + 4); + return this.serializeObject(value, indentationLevel + CrossModelSerializer.INDENTATION_AMOUNT_OBJECT); } else { return JSON.stringify(value); } } private serializeObject(obj: Record, indentationLevel: number): string { - const indentation = ' '.repeat(indentationLevel); + const indentation = CrossModelSerializer.CHAR_INDENTATION.repeat(indentationLevel); const serializedProperties = Object.entries(obj) .sort((left, right) => PROPERTY_ORDER.indexOf(left[0]) - PROPERTY_ORDER.indexOf(right[0])) @@ -64,6 +73,7 @@ export class CrossModelSerializer implements Serializer { const serializedValue = this.serializeValue(value, indentationLevel); + // TODO Refactor CrossModel language so key is same as property name. Then the following lines can be removed. if (key === 'name_val') { key = 'name'; } else if (key === 'name') { @@ -71,21 +81,21 @@ export class CrossModelSerializer implements Serializer { } if (typeof value === 'object') { - return `${indentation}${key}:\n${serializedValue}`; + return `${indentation}${key}:${CrossModelSerializer.CHAR_NEWLINE}${serializedValue}`; } else { return `${indentation}${key}: ${serializedValue}`; } }) .filter(item => item !== undefined); - return serializedProperties.join('\n'); + return serializedProperties.join(CrossModelSerializer.CHAR_NEWLINE); } private serializeArray(arr: any[], indentationLevel: number): string { const serializedItems = arr .map(item => this.serializeValue(item, indentationLevel)) - .map(item => this.changeCharInString(item, indentationLevel + 2, '-')) - .join('\n'); + .map(item => this.changeCharInString(item, indentationLevel + CrossModelSerializer.INDENTATION_AMOUNT_ARRAY, '-')) + .join(CrossModelSerializer.CHAR_NEWLINE); return serializedItems; } diff --git a/extensions/crossmodel-lang/src/model-server/launch.ts b/extensions/crossmodel-lang/src/model-server/launch.ts index 9686a256..6c251778 100644 --- a/extensions/crossmodel-lang/src/model-server/launch.ts +++ b/extensions/crossmodel-lang/src/model-server/launch.ts @@ -11,38 +11,38 @@ import { ModelServer } from './model-server'; const currentConnections: rpc.MessageConnection[] = []; /** - * Creates a socket-based RCP model server that acts as a facade to the Langium-based semantic model index (documents). + * Creates a socket-based RPC model server that acts as a facade to the Langium-based semantic model index (documents). * * @param services language services * @returns a promise that is resolved as soon as the server is shut down or rejects if an error occurs */ export function startModelServer(services: CrossModelLSPServices, workspaceFolder: URI): Promise { - const netServer = net.createServer(socket => createClientConnection(socket, services)); - netServer.listen(0); - netServer.on('listening', () => { - const addressInfo = netServer.address(); - if (!addressInfo) { - console.error('[ModelServer] Could not resolve address info. Shutting down.'); - close(netServer); - return; - } else if (typeof addressInfo === 'string') { - console.error(`[ModelServer] Unexpectedly listening to pipe or domain socket "${addressInfo}". Shutting down.`); - close(netServer); - return; - } - console.log(`[ModelServer] Ready to accept new client requests on port: ${addressInfo.port}`); + const netServer = net.createServer(socket => createClientConnection(socket, services)); + netServer.listen(0); + netServer.on('listening', () => { + const addressInfo = netServer.address(); + if (!addressInfo) { + console.error('[ModelServer] Could not resolve address info. Shutting down.'); + close(netServer); + return; + } else if (typeof addressInfo === 'string') { + console.error(`[ModelServer] Unexpectedly listening to pipe or domain socket "${addressInfo}". Shutting down.`); + close(netServer); + return; + } + console.log(`[ModelServer] Ready to accept new client requests on port: ${addressInfo.port}`); - // write dynamically assigned port to workspace folder to let clients know we are ready to accept connections - writePortFileToWorkspace(workspaceFolder, MODELSERVER_PORT_FILE, addressInfo); - }); - netServer.on('error', err => { - console.error('[ModelServer] Error: ', err); - close(netServer); - }); - return new Promise((resolve, reject) => { - netServer.on('close', () => resolve(undefined)); - netServer.on('error', error => reject(error)); - }); + // Write dynamically assigned port to workspace folder to let clients know we are ready to accept connections + writePortFileToWorkspace(workspaceFolder, MODELSERVER_PORT_FILE, addressInfo); + }); + netServer.on('error', err => { + console.error('[ModelServer] Error: ', err); + close(netServer); + }); + return new Promise((resolve, reject) => { + netServer.on('close', () => resolve(undefined)); + netServer.on('error', error => reject(error)); + }); } /** @@ -53,21 +53,21 @@ export function startModelServer(services: CrossModelLSPServices, workspaceFolde * @returns a promise that is resolved as soon as the connection is closed or rejects if an error occurs */ async function createClientConnection(socket: net.Socket, services: CrossModelLSPServices): Promise { - console.info(`[ModelServer] Starting model server connection for client: '${socket.localAddress}'`); - const connection = createConnection(socket); - currentConnections.push(connection); + console.info(`[ModelServer] Starting model server connection for client: '${socket.localAddress}'`); + const connection = createConnection(socket); + currentConnections.push(connection); - const modelServer = new ModelServer(connection, services.shared.model.ModelService); - connection.onDispose(() => modelServer.dispose()); - socket.on('close', () => modelServer.dispose()); + const modelServer = new ModelServer(connection, services.shared.model.ModelService); + connection.onDispose(() => modelServer.dispose()); + socket.on('close', () => modelServer.dispose()); - connection.listen(); - console.info(`[ModelServer] Connecting to client: '${socket.localAddress}'`); + connection.listen(); + console.info(`[ModelServer] Connecting to client: '${socket.localAddress}'`); - return new Promise((resolve, rejects) => { - connection.onClose(() => resolve(undefined)); - connection.onError(error => rejects(error)); - }); + return new Promise((resolve, rejects) => { + connection.onClose(() => resolve(undefined)); + connection.onError(error => rejects(error)); + }); } /** @@ -77,7 +77,7 @@ async function createClientConnection(socket: net.Socket, services: CrossModelLS * @returns message connection */ function createConnection(socket: net.Socket): rpc.MessageConnection { - return rpc.createMessageConnection(new rpc.SocketMessageReader(socket), new rpc.SocketMessageWriter(socket), console); + return rpc.createMessageConnection(new rpc.SocketMessageReader(socket), new rpc.SocketMessageWriter(socket), console); } /** @@ -86,6 +86,6 @@ function createConnection(socket: net.Socket): rpc.MessageConnection { * @param netServer server to be closed */ function close(netServer: net.Server): void { - currentConnections.forEach(connection => connection.dispose()); - netServer.close(); + currentConnections.forEach(connection => connection.dispose()); + netServer.close(); } diff --git a/packages/model-service/src/node/model-service.ts b/packages/model-service/src/node/model-service.ts index 4c575317..e3811ea1 100644 --- a/packages/model-service/src/node/model-service.ts +++ b/packages/model-service/src/node/model-service.ts @@ -60,16 +60,25 @@ export class ModelServiceImpl implements ModelService, BackendApplicationContrib } protected async connectToServer(port: number): Promise { + // Create the deferred object which exposes the Promise of the connection with the ModelServer. const connected = new Deferred(); + + // Create the socket, reader, writer and rpc-connection. const socket = new net.Socket(); const reader = new rpc.SocketMessageReader(socket); const writer = new rpc.SocketMessageWriter(socket); this.connection = rpc.createMessageConnection(reader, writer); - this.connection.onClose(() => connected.reject('No connection to ModelServer.')); - socket.on('close', () => connected.reject('No connection to ModelServer')); + // Configure connection promise results for the rpc connection. + this.connection.onClose(() => connected.reject('Connection with the ModelServer was closed.')); + this.connection.onError(() => connected.reject('Error occured with the connection to the ModelServer')); + + // Configure connection promise results for the socket. socket.on('ready', () => connected.resolve()); - socket.on('error', error => console.error('Error was thrown on the ModelServer connection: %s; %s', error.name, error.message)); + socket.on('close', () => connected.reject('Socket from ModelService to ModelServer was closed.')); + socket.on('error', error => console.error('Error occurred with the ModelServer socket: %s; %s', error.name, error.message)); + + // Connect to the ModelServer on the given port. socket.connect({ port }); this.connection.listen(); @@ -80,13 +89,14 @@ export class ModelServiceImpl implements ModelService, BackendApplicationContrib async waitForPort(): Promise { // the automatically assigned port is written by the server to a specific file location // we wait for that file to be available and read the port number out of it - // that way we can ensure that the server is ready to accept our connection + // that way we can ensure that the server is ready to accept our connection. const workspace = await this.workspaceServer.getMostRecentlyUsedWorkspace(); if (!workspace) { throw new Error('No workspace set.'); } const portFile = new URI(workspace).path.join(PORT_FOLDER, MODELSERVER_PORT_FILE).fsPath(); const port = await waitForTemporaryFileContent(portFile); + console.debug('Found port number in workspace: %d: ', port); return Number.parseInt(port, 10); }