From 955343da90a05b48261e5cf6639a554b6e23323a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wawrzyniec=20Urba=C5=84czyk?= Date: Wed, 10 Mar 2021 11:09:45 +0100 Subject: [PATCH] Allow visualizations to define context module for preprocessor (https://github.com/enso-org/ide/pull/1291) Ref #2705 Original commit: https://github.com/enso-org/ide/commit/4e7dfd0bd01351ced1348400516927b9e83657eb --- gui/CHANGELOG.md | 6 + gui/docs/product/visualizations.md | 203 ++++++++++-------- gui/src/rust/Cargo.lock | 33 +-- gui/src/rust/ide/Cargo.toml | 4 +- .../rust/ide/src/controller/graph/executed.rs | 4 +- .../rust/ide/src/controller/visualization.rs | 51 +++-- .../src/double_representation/identifier.rs | 14 ++ .../ide/src/double_representation/module.rs | 10 +- gui/src/rust/ide/src/ide/integration.rs | 56 ++--- gui/src/rust/ide/src/model/project.rs | 18 ++ .../ide/src/model/project/synchronized.rs | 1 + gui/src/rust/ide/view/graph-editor/Cargo.toml | 1 + .../src/builtin/visualization/java_script.rs | 13 +- .../visualization/java_script/geoMap.js | 40 +--- .../visualization/java_script/scatterPlot.js | 12 +- .../visualization/java_script/table.js | 21 +- .../visualization/native/bubble_chart.rs | 4 +- .../src/builtin/visualization/native/error.rs | 47 ++-- .../src/component/visualization/container.rs | 11 +- .../src/component/visualization/data.rs | 6 +- .../foreign/java_script/binding.rs | 15 +- .../foreign/java_script/definition.rs | 114 ++-------- .../foreign/java_script/instance.rs | 67 +++--- .../foreign/java_script/visualization.js | 124 ++++++++++- .../src/component/visualization/instance.rs | 84 +++++++- .../src/component/visualization/metadata.rs | 4 +- .../src/component/visualization/path.rs | 22 +- .../rust/ide/view/graph-editor/src/data.rs | 8 +- gui/src/rust/ide/view/graph-editor/src/lib.rs | 10 +- .../view/src/debug_scenes/visualization.rs | 3 +- gui/src/rust/ide/view/src/documentation.rs | 3 +- 31 files changed, 618 insertions(+), 391 deletions(-) diff --git a/gui/CHANGELOG.md b/gui/CHANGELOG.md index a9766a542880..3432943eb820 100644 --- a/gui/CHANGELOG.md +++ b/gui/CHANGELOG.md @@ -14,6 +14,11 @@ unnecessary library imports when selecting hints from node searcher. This makes the generated textual code easier to read and reduces likelihood of accidental name collision. +- [Visualizations can define context for preprocessor evaluation][1291]. Users + can now decide what module's context should be used for visualization + preprocessor. This allows providing visualization with standard library + functionalities or defining utilities that are shared between multiple + visualizations. #### EnsoGL (rendering engine) @@ -24,6 +29,7 @@ you can find their release notes [here](https://github.com/enso-org/enso/blob/main/RELEASES.md). [1209]: https://github.com/enso-org/ide/pull/1209 +[1291]: https://github.com/enso-org/ide/pull/1291
diff --git a/gui/docs/product/visualizations.md b/gui/docs/product/visualizations.md index 1f2f5d8b5a7f..32c36907a7bd 100644 --- a/gui/docs/product/visualizations.md +++ b/gui/docs/product/visualizations.md @@ -8,6 +8,7 @@ tags: [product] # Visualization Workflow ## Purpose of visualizations + Visualizations have two main purposes: - **Display results of nodes** @@ -27,14 +28,14 @@ Visualizations have two main purposes: used to interactively generate a table of numbers. Image visualizations can behave like an image editor, etc. - ## Visualization Display Forms + Visualizations can be displayed in the following ways: -- **Attached to nodes** - In this mode, visualizations display the most recent result of the node. They - behave like an integrated part of the node. Whenever you move the node, the - visualization moves as well. This mode can be toggled by tapping the spacebar. +- **Attached to nodes** In this mode, visualizations display the most recent + result of the node. They behave like an integrated part of the node. Whenever + you move the node, the visualization moves as well. This mode can be toggled + by tapping the spacebar. - **Fullscreen** Visualization attached to node can grow (animate) to ocupy full IDE visual @@ -47,9 +48,9 @@ Visualizations can be displayed in the following ways: - **Detached** Visualizations attached to nodes can be detached, scaled, and placed freely across the visual canvas (we might introduce a special place where you can put - such visualizations). This is useful when defining dashboards or reports. - We also plan to provide a notebook-like experience where you can write text - mixed with visualizations (including widgets for an interactive experience). + such visualizations). This is useful when defining dashboards or reports. We + also plan to provide a notebook-like experience where you can write text mixed + with visualizations (including widgets for an interactive experience). - **Widgets** In this mode visualizations behave like nodes but do not display expressions. @@ -65,8 +66,8 @@ Visualizations can be displayed in the following ways: user clicks the map to define locations, the data could be a string literal containing locations encoded in JSON. - ### Choosing a Visualization Type. + When a new data is provided to a visualization, the visualization registry searches for all visualizations that match it (see visualization registry to learn more). For example, when a data of type `[Int]` (list of ints) is @@ -78,6 +79,7 @@ visualization has a drop-down menu allowinh the user switching to another visualization type. ### Active Visualizations + When visualizations are displayed on the stage, they are not active by default, which means, they do not capture keyboard shortcuts. Visualization becomes active when user clicks it. Visualizations are deactivated by clicking in the @@ -87,8 +89,8 @@ border (to be defined). Active visualizations capture all keyboard shortcuts, but the space bar presses. Fullscreen visualizations are considered active by default. - ## HTML and Native Visualizations + There are two main types of visualizations - Html and Native. The later uses the BaseGL shape API to draw on the screen. We prefer the later as it integrates tightly with our framework and allows for much better performance. However, @@ -105,33 +107,37 @@ and bottom HTML layer. The HTML visualizations are created and displayed on the bottom layer by default. Whenever an HTML visualization gets active, it should be moved to the top layer. - ## Visualization Registry + Visualizations are user-defined. Enso ships with a set of predefined visualizations, but they are in no way different than user-defined, they are just defined for you. Visualizations can be defined either as HTML or native -visualization and can be defined in JS or WASM (or any language that compiles -to one of these). Visualizations are stored on disk on the server-side and are +visualization and can be defined in JS or WASM (or any language that compiles to +one of these). Visualizations are stored on disk on the server-side and are provided to the GUI by the server. Users can upload their custom visualizations as well. Each visualization is registered in the visualization map. The map maps an Enso type to a set of visualizations defined for that type. The type might be very generic, like `[a]` (which in Enso terms means list of any elements). -### Defining a Visualization -Visualizations is planned to be defined both with Enso and JavaScript but, for -now, only JavaScript visualizations are supported. +## Defining a Visualization + +Currently only JavaScript visualizations can be defined. Support for native +visualizations is planned. + +### Defining a JavaScript Visualization -Because IDE lacks support for editing any other file besides `Main.enso`, the -user has to create it outside of IDE in the `visualization` folder of the Enso -project, as demonstrated bellow. +JavaScript visualizations are defined by placing `*.js` files in the +`visualization` subfolder in the project's root directory. As IDE currently +allows only editing `Main.enso` file, users have to create `.js` file manually, +editing it outside IDE. -#### Custom Visualization Example +## Custom JavaScript Visualization Example Every visualization must reside in the `visualization` folder of the user's project. For instance: ``` -└─ ProjectName +└─ Project_Name ├─ src │ └─ Main.enso └─ visualization @@ -139,48 +145,48 @@ project. For instance: ``` Visualizations can be defined as a JavaScript function which returns a class of -a shape specified below. Consider the following definition: +a shape specified below. Consider the following sample definition: ```javascript -console.log("Hi, this definition is being registered now!") +console.log("Hi, this definition is being registered now!"); return class BubbleVisualization extends Visualization { - static inputType = "Any" - - onDataReceived(data) { - const xmlns = "http://www.w3.org/2000/svg"; - while (this.dom.firstChild) { - this.dom.removeChild(this.dom.lastChild); - } - const width = this.dom.getAttributeNS(null, "width"); - const height = this.dom.getAttributeNS(null, "height"); - const svgElem = document.createElementNS(xmlns, "svg"); - svgElem.setAttributeNS(null, "id" , "vis-svg"); - svgElem.setAttributeNS(null, "viewBox", "0 0 " + width + " " + height); - svgElem.setAttributeNS(null, "width" , "100%"); - svgElem.setAttributeNS(null, "height" , "100%"); - this.dom.appendChild(svgElem); - data.forEach(data => { - const bubble = document.createElementNS(xmlns,"circle"); - bubble.setAttributeNS(null,"stroke", "black"); - bubble.setAttributeNS(null,"fill" , "red"); - bubble.setAttributeNS(null,"r" , data[2]); - bubble.setAttributeNS(null,"cx" , data[0]); - bubble.setAttributeNS(null,"cy" , data[1]); - svgElem.appendChild(bubble); - }); - } + static inputType = "Any"; - setSize(size) { - this.dom.setAttributeNS(null, "width", size[0]); - this.dom.setAttributeNS(null, "height", size[1]); + onDataReceived(data) { + const xmlns = "http://www.w3.org/2000/svg"; + while (this.dom.firstChild) { + this.dom.removeChild(this.dom.lastChild); } -} + const width = this.dom.getAttributeNS(null, "width"); + const height = this.dom.getAttributeNS(null, "height"); + const svgElem = document.createElementNS(xmlns, "svg"); + svgElem.setAttributeNS(null, "id", "vis-svg"); + svgElem.setAttributeNS(null, "viewBox", "0 0 " + width + " " + height); + svgElem.setAttributeNS(null, "width", "100%"); + svgElem.setAttributeNS(null, "height", "100%"); + this.dom.appendChild(svgElem); + data.forEach((data) => { + const bubble = document.createElementNS(xmlns, "circle"); + bubble.setAttributeNS(null, "stroke", "black"); + bubble.setAttributeNS(null, "fill", "red"); + bubble.setAttributeNS(null, "r", data[2]); + bubble.setAttributeNS(null, "cx", data[0]); + bubble.setAttributeNS(null, "cy", data[1]); + svgElem.appendChild(bubble); + }); + } + + setSize(size) { + this.dom.setAttributeNS(null, "width", size[0]); + this.dom.setAttributeNS(null, "height", size[1]); + } +}; ``` In particular: -- [Required] **Source code** +- ### [Required] Source code Visualization definition has to be a valid body of JavaScript function which returns a class definition. Instances of that class will be considered @@ -188,20 +194,37 @@ In particular: state across visualizations of the same type, but you are highly advised not to do so. -- [Required] **`Visualization` superclass** - +- ### [Required] `Visualization` superclass + The class returned by the definition function should extend the predefined `Visualization` class. Classes which do not extend it, will not be registered as visualizations. The superclass defines a default constructor and a set of utilities: - - The `setPreprocessor(code)` a method allowing setting an Enso code - which will be evaluated on the server-side before sending data to - visualization. See also [Lazy visualizations](#lazy-visualizations) section. - - The `dom` field, which will be initialized in the constructor to the DOM - symbol used to host the visualization content. You are free to modify the - DOM element, including adding other elements as its children. -- [Optional] **Field `label`** + - #### Method `setPreprocessorCode(code)` + Set an Enso code which will be evaluated on the server-side before sending + data to visualization. If not called, a default unspecified code is used + that will provide some JSON representation of the value. See + [Lazy visualizations](#lazy-visualizations) section for details. + - #### Method `setPreprocessorModule(module)` + Define in which module's context the preprocessor code should be evaluated. + If not called, the `Main` module of the project that defines visualization + will be used. See [Lazy visualizations](#lazy-visualizations) section for + details. + - #### Method `setPreprocessor(code,mode)` + Set both code and its module context at once. If both need to be updated, + using this method can save an update processing and needless evaluation. + Note that using both `setPreprocessorCode` and `setPreprocessorModule` from + the visualization's custom constructor will not cause any unnecessary + updates, as the preprocessor is applied only after visualization is fully + constructed. See [Lazy visualizations](#lazy-visualizations) section for + details. + - #### Field `dom` + It is initialized in the constructor to the DOM symbol used to host the + visualization content. Users are free to modify the DOM element, including + adding other elements as its children. + +- ### [Optional] Field `label` The static field `label` is an user-facing name used to identify the visualization. You are not allowed to define several visualizations of the @@ -209,16 +232,16 @@ In particular: will be inferred from the class name by splitting the camel-case name into chunks and converting them to lowercase string. -- [Optional] **Field `inputType`** +- ### [Optional] Field `inputType` The static field `inputType` is used to determine which Enso data types this visualization can be used for. Its value should be a valid Enso type, like - "String | Int". In case the field is an empty string or it is missing, it will - default to "Any", which is a type containing all other types. It is a rare - case when you want to define a visualization which is able to work with just - any data type, so you are highly advised to provide the type definition. + `"String | Int"`. In case the field is an empty string or it is missing, it + will default to `"Any"`, which is a type containing all other types. It is a + rare case when you want to define a visualization which is able to work with + just any data type, so you are highly advised to provide the type definition. -- [Optional] **Field `inputFormat`** +- ### [Optional] Field `inputFormat` The static field `inputFormat` is used to determine what format the data should be provided to the `onDataReceived` function. Currently, the only valid @@ -226,29 +249,30 @@ In particular: In the later case, it is up to the visualization author to manage the binary stream received from the server. -- [Optional] **Constructor** +- ### [Optional] Constructor The visualization will be instantiated by providing the constructor with a configuration object. The shape of the configuration object is not part of the public API and can change between releases of this library. You have to pass it unchanged to the superclass constructor. -- [Optional] **Function `onDataReceived`** +- ### [Optional] Function `onDataReceived` The `onDataReceived(data)` method is called on every new data chunk received from the server. Note that the visualization will receive the "full data" if you are not using the `setPreprocessor` method. -- [Optional] **Function `setSize`** +- ### [Optional] Function `setSize` The `setSize(size)` method is called on every size change of the visualization. You should not draw outside of the provided area, however, if you do so, it will be clipped to the provided area automatically. The `size` parameter contains two fields `width` and `height` expressed in pixels. -### Sending Data to Visualizations +## Sending Data to Visualizations + +### Lazy Visualizations -#### Lazy Visualizations Very important information is how visualization architecture works to make them interactive and fast. Whenever new data is computed by the compiler and visualization is attached to it, it is sent to GUI to be displayed. However, @@ -258,28 +282,27 @@ This code is part of the visualization definition and is stored server-side. Visualizations are allowed to change the code at runtime (in JavaScript visualization you may use the `setPreprocessor` method). This code defines an Enso function, which will be run by the compiler on data the visualization is -attached to. Only the results of this code will be sent to the GUI. In the case -of the JSON input format, the result of the call should be a valid JSON string. -The code will be evaluated in the context of the `Main` module in the -project where visualization is defined - you may use any symbol defined or -imported in that module. - -For example, imagine you want to display a heatmap of 10 million points on -a map, and these points change rapidly. Sending such an amount of information -via WebSocket could be too much, and you (as the visualization author) might -decide that the visualization image should be generated on the server, and your +attached to. Only the results of this code will be sent to the GUI. In the case +of the JSON input format, the result of the call should be a valid JSON string. +The code will be evaluated in the context of the `Main` module in the project +where visualization is defined - you may use any symbol defined or imported in +that module. + +For example, imagine you want to display a heatmap of 10 million points on a +map, and these points change rapidly. Sending such an amount of information via +WebSocket could be too much, and you (as the visualization author) might decide +that the visualization image should be generated on the server, and your visualization is meant only to display the resulting image. In such a scenario, -you can define in your visualization an Enso function which will compute the -the image on the server! - +you can define in your visualization an Enso function which will compute the the +image on the server! +### Binary and Text (JSON) Formats -#### Binary and Text (JSON) Formats Each visualization can choose whether it supports either binary or JSON input. The input format defaults to JSON. The data from the server is always sent to GUI in a binary channel, however, when JSON format is selected, it is first -converted to JSON representation on the server side. We can assume that all -Enso data types have defined conversion to JSON by default. If the visualization +converted to JSON representation on the server side. We can assume that all Enso +data types have defined conversion to JSON by default. If the visualization input is defined as JSON input, the binary stream will be converted to JSON by the GUI engine before passing to visualization. It is up to the visualization -author to handle the textual or binary form. +author to handle the textual or binary form. diff --git a/gui/src/rust/Cargo.lock b/gui/src/rust/Cargo.lock index cd3891b18736..c7777f7f5389 100644 --- a/gui/src/rust/Cargo.lock +++ b/gui/src/rust/Cargo.lock @@ -470,7 +470,7 @@ dependencies = [ name = "enso-args" version = "0.1.0" dependencies = [ - "enso-logger 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "enso-logger 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "enso-prelude 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "ensogl 0.1.0", ] @@ -507,7 +507,7 @@ dependencies = [ "Inflector 0.11.4 (registry+https://github.com/rust-lang/crates.io-index)", "enso-callback 0.1.0", "enso-generics 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "enso-logger 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "enso-logger 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "enso-prelude 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "ensogl-system-web 0.1.0", "keyboard-types 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -529,7 +529,7 @@ dependencies = [ [[package]] name = "enso-logger" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "enso-prelude 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", @@ -592,7 +592,7 @@ dependencies = [ "bytes 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.19 (registry+https://github.com/rust-lang/crates.io-index)", "enso-data 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "enso-logger 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "enso-logger 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "enso-prelude 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "enso-shapely 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "ensogl-build-utilities 0.1.0", @@ -647,7 +647,7 @@ version = "0.1.0" dependencies = [ "enso-automata 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "enso-frp 0.1.0", - "enso-logger 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "enso-logger 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "enso-prelude 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "ensogl-system-web 0.1.0", "js-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)", @@ -664,7 +664,7 @@ version = "0.1.0" dependencies = [ "enso-automata 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "enso-frp 0.1.0", - "enso-logger 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "enso-logger 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "enso-prelude 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "enso-shortcuts 0.1.0", "ensogl-system-web 0.1.0", @@ -681,7 +681,7 @@ name = "enso-span-tree-example" version = "0.1.0" dependencies = [ "ast 0.1.0", - "enso-logger 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "enso-logger 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "enso-prelude 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "ensogl-system-web 0.1.0", "span-tree 0.1.0", @@ -728,7 +728,7 @@ dependencies = [ "enso-data 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "enso-frp 0.1.0", "enso-generics 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "enso-logger 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "enso-logger 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "enso-optics 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "enso-prelude 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "enso-shapely 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -761,7 +761,7 @@ name = "ensogl-examples" version = "0.1.0" dependencies = [ "enso-frp 0.1.0", - "enso-logger 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "enso-logger 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "enso-prelude 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "enso-shapely 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "ensogl-core 0.1.0", @@ -782,7 +782,7 @@ name = "ensogl-gui-components" version = "0.1.0" dependencies = [ "enso-frp 0.1.0", - "enso-logger 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "enso-logger 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "enso-prelude 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "enso-shapely 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "enso-types 0.1.0", @@ -799,7 +799,7 @@ dependencies = [ "async-std 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "enso-data 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "enso-logger 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "enso-logger 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "enso-prelude 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", "gloo-timers 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1265,7 +1265,7 @@ dependencies = [ "enso-callback 0.1.0", "enso-data 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "enso-frp 0.1.0", - "enso-logger 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "enso-logger 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "enso-prelude 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "enso-protocol 0.1.0", "enso-shapely 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1281,7 +1281,7 @@ dependencies = [ "futures 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "fuzzly 0.1.0", "ide-view 0.1.0", - "itertools 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", + "itertools 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "js-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)", "json-rpc 0.1.0", "mockall 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1306,7 +1306,7 @@ version = "0.1.0" dependencies = [ "ast 0.1.0", "enso-frp 0.1.0", - "enso-logger 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "enso-logger 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "enso-prelude 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "enso-protocol 0.1.0", "enso-shapely 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1335,7 +1335,7 @@ dependencies = [ "ast 0.1.0", "enso-args 0.1.0", "enso-frp 0.1.0", - "enso-logger 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "enso-logger 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "enso-prelude 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "enso-protocol 0.1.0", "enso-shapely 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1344,6 +1344,7 @@ dependencies = [ "ensogl-text 0.1.0", "ensogl-text-msdf-sys 0.1.0", "ensogl-theme 0.1.0", + "failure 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", "js-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)", "nalgebra 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.116 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3223,7 +3224,7 @@ dependencies = [ "checksum enso-automata 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2f8d432048ea9d4c4b5f84fd246303b74c8a52d9e5878f593fef540a67faff24" "checksum enso-data 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "5baf4b3bd2a814441265d47d1d5ffb1d38bd194043165be0b68c2ad4ad7e73de" "checksum enso-generics 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0bf6f66c10edfaa8b55292053192112d0796c12b2aeab0e2f51dae5eec729d3b" -"checksum enso-logger 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "115416c9661c7b35b29fb3385c9b40e55fabc008e74c9bf0eb350e551297c481" +"checksum enso-logger 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a287f9ae80537a9ee4847b1a12e16e7e2dc94d576505ae3c13c6dcb9437bcbb3" "checksum enso-macro-utils 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "616e099985be92ef3de01f4c062868b46ab06868e1fc9cd077050709ccc2d9ae" "checksum enso-optics 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0f403efb0c27b67eb8cfec664ed7456224d564b3095268d856023648848d4139" "checksum enso-prelude 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "b3bd9bdcf47dc157a85cbad266966ba2587b67088e5cf49ba3621c94ab01e2ab" diff --git a/gui/src/rust/ide/Cargo.toml b/gui/src/rust/ide/Cargo.toml index 8da1bac8c96d..dc02b56fa67b 100644 --- a/gui/src/rust/ide/Cargo.toml +++ b/gui/src/rust/ide/Cargo.toml @@ -13,7 +13,7 @@ enso-args = { path = "lib/args" } enso-callback = { path = "../lib/callback" } enso-data = { version = "0.1.4" } enso-frp = { path = "../lib/frp" } -enso-logger = { version = "0.2.2" } +enso-logger = { version = "0.2.3" } enso-prelude = { version = "0.1.10" } enso-shapely = { version = "0.1.4" } ensogl = { path = "../ensogl" } @@ -36,7 +36,7 @@ console_error_panic_hook = { version = "0.1.6" } failure = { version = "0.1.6" } flo_stream = { version = "0.4.0" } futures = { version = "0.3.1" } -itertools = { version = "0.8.1" } +itertools = { version = "0.10.0" } js-sys = { version = "0.3.28" } mockall = { version = "0.7.1", features = ["nightly"] } nalgebra = { version = "0.21.1", features = ["serde-serialize"] } diff --git a/gui/src/rust/ide/src/controller/graph/executed.rs b/gui/src/rust/ide/src/controller/graph/executed.rs index 308f177f490a..ab58207b46da 100644 --- a/gui/src/rust/ide/src/controller/graph/executed.rs +++ b/gui/src/rust/ide/src/controller/graph/executed.rs @@ -141,8 +141,8 @@ impl Handle { /// Modify preprocessor code in visualization. See also /// [`model::ExecutionContext::modify_visualization`]. pub async fn set_visualization_preprocessor - (&self, id:VisualizationId, code:String) -> FallibleResult { - self.execution_ctx.modify_visualization(id,Some(code),None).await + (&self, id:VisualizationId, code:String, module:model::module::QualifiedName) -> FallibleResult { + self.execution_ctx.modify_visualization(id,Some(code),Some(module)).await } /// Subscribe to updates about changes in this executed graph. diff --git a/gui/src/rust/ide/src/controller/visualization.rs b/gui/src/rust/ide/src/controller/visualization.rs index 5d3f133f9bed..b4c97a4a02ad 100644 --- a/gui/src/rust/ide/src/controller/visualization.rs +++ b/gui/src/rust/ide/src/controller/visualization.rs @@ -8,7 +8,6 @@ use crate::prelude::*; use crate::constants::VISUALIZATION_DIRECTORY; use enso_protocol::language_server; -use ide_view::graph_editor::data::enso; use ide_view::graph_editor::component::visualization::definition; use ide_view::graph_editor::component::visualization; use std::rc::Rc; @@ -22,14 +21,30 @@ use std::rc::Rc; /// Enumeration of errors used in `Visualization Controller`. #[derive(Debug,Fail)] #[allow(missing_docs)] -pub enum VisualizationError { +pub enum Error { #[fail(display = "Visualization \"{}\" not found.", identifier)] NotFound { - identifier : VisualizationPath + identifier:VisualizationPath + }, + #[fail(display = "JavaScript visualization \"{}\" failed to be prepared.", identifier)] + Preparation { + identifier:VisualizationPath, + #[cause] + cause:failure::Error, }, #[fail(display = "JavaScript visualization \"{}\" failed to be instantiated.", identifier)] - InstantiationError { - identifier : VisualizationPath + Instantiation { + identifier:VisualizationPath + }, +} + +impl Error { + /// Construct a new error regarding visualization preparation. + pub fn js_preparation_error + (identifier:VisualizationPath, error:visualization::foreign::java_script::definition::Error) + -> Self { + let cause = failure::format_err!("{}",error); + Self::Preparation {identifier,cause} } } @@ -130,21 +145,18 @@ impl Handle { let embedded_visualizations = self.embedded_visualizations.borrow(); let result = embedded_visualizations.get(identifier); let identifier = visualization.clone(); - let error = || VisualizationError::NotFound{identifier}.into(); + let error = || Error::NotFound{identifier}.into(); result.cloned().ok_or_else(error) }, VisualizationPath::File(path) => { + let project = visualization::path::Project::CurrentProject; let js_code = self.language_server_rpc.read_file(&path).await?.contents; - let identifier = visualization.clone(); - let error = |_| VisualizationError::InstantiationError {identifier}.into(); - // FIXME: provide real library name. The name set here is discarded anyway in - // [`ide::integration::Model::prepare_visualization`] and set the Main of the - // current project. All those should be fixed in - // https://github.com/enso-org/ide/issues/1167 - let module = enso::builtin_library(); - // TODO: this is wrong. This is translated to InstantiationError and it is preparation error : - let js_class = visualization::java_script::Definition::new(module,&js_code).map_err(error); - js_class.map(|t| t.into()) + let wrap_error = |err| { + Error::js_preparation_error(visualization.clone(),err).into() + }; + visualization::java_script::Definition::new(project,&js_code) + .map(Into::into) + .map_err(wrap_error) } } } @@ -164,6 +176,7 @@ mod tests { use enso_protocol::language_server::Path; use ide_view::graph_editor::builtin; use ide_view::graph_editor::component::visualization; + use ide_view::graph_editor::component::visualization::java_script as js_vis; use json_rpc::expect_call; use wasm_bindgen_test::wasm_bindgen_test_configure; @@ -173,6 +186,7 @@ mod tests { #[wasm_bindgen_test(async)] async fn list_and_load() { + let mock_client = language_server::MockClient::default(); let root_id = uuid::Uuid::default(); @@ -226,8 +240,9 @@ mod tests { assert_eq!(visualizations[2], VisualizationPath::File(path1)); assert_eq!(visualizations.len(),3); - let javascript_vis0 = visualization::java_script::Definition::new("builtin", &file_content0); - let javascript_vis1 = visualization::java_script::Definition::new("builtin", &file_content1); + let owner = visualization::Project::CurrentProject; + let javascript_vis0 = js_vis::Definition::new(owner.clone_ref(),&file_content0); + let javascript_vis1 = js_vis::Definition::new(owner,&file_content1); let javascript_vis0 = javascript_vis0.expect("Couldn't create visualization class."); let javascript_vis1 = javascript_vis1.expect("Couldn't create visualization class."); let javascript_vis0:visualization::Definition = javascript_vis0.into(); diff --git a/gui/src/rust/ide/src/double_representation/identifier.rs b/gui/src/rust/ide/src/double_representation/identifier.rs index 832b8d820382..69afcfb0c8eb 100644 --- a/gui/src/rust/ide/src/double_representation/identifier.rs +++ b/gui/src/rust/ide/src/double_representation/identifier.rs @@ -239,6 +239,20 @@ impl AsRef for ReferentName { } } +impl TryFrom<&str> for ReferentName { + type Error = NotReferentName; + fn try_from(value:&str) -> Result { + Self::new(value) + } +} + +impl TryFrom for ReferentName { + type Error = NotReferentName; + fn try_from(value:String) -> Result { + Self::new(value) + } +} + impl From for String { fn from(name:ReferentName) -> Self { name.0 diff --git a/gui/src/rust/ide/src/double_representation/module.rs b/gui/src/rust/ide/src/double_representation/module.rs index 5b954fbb2902..367431fb796b 100644 --- a/gui/src/rust/ide/src/double_representation/module.rs +++ b/gui/src/rust/ide/src/double_representation/module.rs @@ -153,10 +153,18 @@ impl QualifiedName { QualifiedName {project_name,id} } + /// Create a qualified name for the project's main module. + /// + /// It is special, as its name consists only from the project name, unlike other modules' + /// qualified names. + pub fn new_main(project_name:ReferentName) -> QualifiedName { + Self::new(project_name, Id::new(std::iter::empty())) + } + /// Constructs a qualified name from its text representation. /// /// Fails, if the text is not a valid module's qualified name. - pub fn from_text(text:impl Str) -> FallibleResult { + pub fn from_text(text:impl AsRef) -> FallibleResult { use ast::opr::predefined::ACCESS; let text = text.as_ref(); diff --git a/gui/src/rust/ide/src/ide/integration.rs b/gui/src/rust/ide/src/ide/integration.rs index dc8ded1907c2..022ea1dc272a 100644 --- a/gui/src/rust/ide/src/ide/integration.rs +++ b/gui/src/rust/ide/src/ide/integration.rs @@ -246,8 +246,8 @@ impl Integration { // === Setting Visualization Preprocessor === frp::extend! { network - eval editor_outs.visualization_preprocessor_changed ([model]((node_id,code)) { - if let Err(err) = model.visualization_preprocessor_changed(*node_id,code) { + eval editor_outs.visualization_preprocessor_changed ([model]((node_id,preprocessor)) { + if let Err(err) = model.visualization_preprocessor_changed(*node_id,preprocessor) { error!(model.logger, "Error when handling request for setting new \ visualization's preprocessor code: {err}"); } @@ -675,15 +675,16 @@ impl Model { /// Mark node as erroneous if given payload contains an error. fn set_error - (&self, node_id:graph_editor::NodeId, error:Option<&ExpressionUpdatePayload>) -> FallibleResult { + (&self, node_id:graph_editor::NodeId, error:Option<&ExpressionUpdatePayload>) + -> FallibleResult { let error = self.convert_payload_to_error_view(error,node_id); self.view.graph().set_node_error_status(node_id,error.clone()); if error.is_some() && !self.error_visualizations.contains_key(&node_id) { - let endpoint = self.view.graph().frp.set_error_visualization_data.clone_ref(); - let preprocessor = graph_editor::builtin::visualization::native::error::PREPROCESSOR; - let preprocessor = graph_editor::data::enso::Code::new(preprocessor); - let metadata = visualization::Metadata {preprocessor}; - self.attach_visualization(node_id,&metadata,endpoint,self.error_visualizations.clone_ref())?; + use graph_editor::builtin::visualization::native::error; + let endpoint = self.view.graph().frp.set_error_visualization_data.clone_ref(); + let metadata = error::metadata(); + let vis_map = self.error_visualizations.clone_ref(); + self.attach_visualization(node_id,&metadata,endpoint,vis_map)?; } else if error.is_none() && self.error_visualizations.contains_key(&node_id) { self.detach_visualization(node_id,self.error_visualizations.clone_ref())?; } @@ -1143,14 +1144,28 @@ impl Model { }); } - fn visualization_preprocessor_changed(&self, node_id:graph_editor::NodeId, code:&graph_editor::data::enso::Code) - -> FallibleResult { + fn resolve_visualization_context + (&self, context:&visualization::instance::ContextModule) + -> FallibleResult { + use visualization::instance::ContextModule::*; + match context { + ProjectMain => self.project.main_module(), + Specific(module_name) => model::module::QualifiedName::from_text(module_name), + } + } + + fn visualization_preprocessor_changed + ( &self + , node_id : graph_editor::NodeId + , preprocessor : &visualization::instance::PreprocessorConfiguration + ) -> FallibleResult { if let Some(visualization) = self.visualizations.get_copied(&node_id) { let logger = self.logger.clone_ref(); let controller = self.graph.clone_ref(); - let code_string = AsRef::::as_ref(code).to_string(); + let code = preprocessor.code.deref().into(); + let module = self.resolve_visualization_context(&preprocessor.module)?; executor::global::spawn(async move { - let result = controller.set_visualization_preprocessor(visualization,code_string); + let result = controller.set_visualization_preprocessor(visualization,code,module); if let Err(err) = result.await { error!(logger, "Error when setting visualization preprocessor: {err}"); } @@ -1274,21 +1289,10 @@ impl Model { fn prepare_visualization (&self, node_id:graph_editor::NodeId, metadata:&visualization::Metadata) -> FallibleResult { - use crate::model::module::QualifiedName; - - // TODO [mwu] - // Currently it is not possible to: - // * enter other module than the initial (namely, "Main") - // * describe that visualization's expression wishes to be evaluated in any other - // context. - // Because of that for now we will just hardcode the `visualization_module` using - // fixed defaults. In future this will be changed, then the editor will also get access - // to the customised values. - let project_name:String = self.project.name().into(); - let module_name = crate::ide::INITIAL_MODULE_NAME; - let visualisation_module = QualifiedName::from_segments(project_name,&[module_name])?; + let module_designation = &metadata.preprocessor.module; + let visualisation_module = self.resolve_visualization_context(module_designation)?; let id = VisualizationId::new_v4(); - let expression = metadata.preprocessor.to_string(); + let expression = metadata.preprocessor.code.to_string(); let ast_id = self.get_controller_node_id(node_id)?; Ok(Visualization{ast_id,expression,id,visualisation_module}) } diff --git a/gui/src/rust/ide/src/model/project.rs b/gui/src/rust/ide/src/model/project.rs index fbdeea596909..8962935ddd83 100644 --- a/gui/src/rust/ide/src/model/project.rs +++ b/gui/src/rust/ide/src/model/project.rs @@ -24,6 +24,7 @@ use uuid::Uuid; #[automock] pub trait API:Debug { /// Project's name + // TODO [mwu] This should return Rc. fn name(&self) -> ImString; /// Get Language Server JSON-RPC Connection for this project. @@ -67,6 +68,23 @@ pub trait API:Debug { (&self, path:&model::module::Path) -> crate::model::module::QualifiedName { path.qualified_module_name(self.name().deref()) } + + /// Get qualified name of the project's `Main` module. + /// + /// This module is special, as it needs to be referred by the project name itself. + fn main_module(&self) -> FallibleResult { + let main = std::iter::once(crate::ide::INITIAL_MODULE_NAME); + model::module::QualifiedName::from_segments(self.name(),main) + + // TODO [mwu] The code below likely should be preferred but does not work + // because language server does not support using project name + // for project's main module in some contexts. + // This is tracked by: https://github.com/enso-org/enso/issues/1543 + // use model::module::QualifiedName; + // ReferentName::try_from(self.name().as_str()) + // .map(QualifiedName::new_main) + // .map_err(Into::into) + } } impl Debug for MockAPI { diff --git a/gui/src/rust/ide/src/model/project/synchronized.rs b/gui/src/rust/ide/src/model/project/synchronized.rs index 0c5a551edf2b..9b8ea6d62232 100644 --- a/gui/src/rust/ide/src/model/project/synchronized.rs +++ b/gui/src/rust/ide/src/model/project/synchronized.rs @@ -209,6 +209,7 @@ impl Project { use enso_protocol::binary::Notification; match event { Event::Notification(Notification::VisualizationUpdate {context,data}) => { + debug!(logger, "Visualization binary data: {String::from_utf8_lossy(data.as_ref())}"); let data = VisualizationUpdateData::new(data); if let Some(execution_contexts) = weak_execution_contexts.upgrade() { let result = execution_contexts.dispatch_visualization_update(context,data); diff --git a/gui/src/rust/ide/view/graph-editor/Cargo.toml b/gui/src/rust/ide/view/graph-editor/Cargo.toml index 3ecabf50c2df..e77fee6523f4 100644 --- a/gui/src/rust/ide/view/graph-editor/Cargo.toml +++ b/gui/src/rust/ide/view/graph-editor/Cargo.toml @@ -21,6 +21,7 @@ ensogl-gui-components = { version = "0.1.0", path = "../../../ensogl/lib/compone ensogl-text = { version = "0.1.0", path = "../../../ensogl/lib/text" } ensogl-text-msdf-sys = { version = "0.1.0", path = "../../../ensogl/lib/text/msdf-sys" } ensogl-theme = { version = "0.1.0", path = "../../../ensogl/lib/theme" } +failure = { version = "0.1.8" } span-tree = { version = "0.1.0", path = "../../lib/span-tree" } js-sys = { version = "0.3.28" } nalgebra = { version = "0.21.1", features = ["serde-serialize"] } diff --git a/gui/src/rust/ide/view/graph-editor/src/builtin/visualization/java_script.rs b/gui/src/rust/ide/view/graph-editor/src/builtin/visualization/java_script.rs index 9b46da378b17..93a4a92fba92 100644 --- a/gui/src/rust/ide/view/graph-editor/src/builtin/visualization/java_script.rs +++ b/gui/src/rust/ide/view/graph-editor/src/builtin/visualization/java_script.rs @@ -2,7 +2,6 @@ // TODO remove once we have proper visualizations or replace with a nice d3 example. // These implementations are neither efficient nor pretty, but get the idea across. -use crate::data::enso; use crate::component::visualization; @@ -18,7 +17,7 @@ pub fn table_visualization() -> visualization::java_script::FallibleDefinition { let source = include_str!("java_script/table.js"); let source = format!("{}{}{}",loading_scripts,scrollable,source); - visualization::java_script::Definition::new(enso::builtin_library(),source) + visualization::java_script::Definition::new_builtin(source) } /// Return a `JavaScript` Scatter plot visualization. @@ -27,7 +26,7 @@ pub fn scatter_plot_visualization() -> visualization::java_script::FallibleDefin let source = include_str!("java_script/scatterPlot.js"); let source = format!("{}{}",loading_scripts,source); - visualization::java_script::Definition::new(enso::builtin_library(),source) + visualization::java_script::Definition::new_builtin(source) } /// Return a `JavaScript` Histogram visualization. @@ -36,7 +35,7 @@ pub fn histogram_visualization() -> visualization::java_script::FallibleDefiniti let source = include_str!("java_script/histogram.js"); let source = format!("{}{}",loading_scripts,source); - visualization::java_script::Definition::new(enso::builtin_library(),source) + visualization::java_script::Definition::new_builtin(source) } /// Return a `JavaScript` Map visualization. @@ -45,14 +44,14 @@ pub fn geo_map_visualization() -> visualization::java_script::FallibleDefinition let source = include_str!("java_script/geoMap.js"); let source = format!("{}{}",loading_scripts,source); - visualization::java_script::Definition::new(enso::builtin_library(),source) + visualization::java_script::Definition::new_builtin(source) } /// Return a `JavaScript` Bubble visualization. This should not be used as it is a demo visualization. pub fn bubble_visualization() -> visualization::java_script::FallibleDefinition { let source = include_str!("java_script/bubbleVisualization.js"); - visualization::java_script::Definition::new(enso::builtin_library(),source) + visualization::java_script::Definition::new_builtin(source) } /// Return an empty minimal `JavaScript` visualization. This should not be used except for testing. @@ -62,5 +61,5 @@ pub fn empty_visualization() -> visualization::java_script::FallibleDefinition { return EmptyVisualization; "#; - visualization::java_script::Definition::new(enso::builtin_library(),source) + visualization::java_script::Definition::new_builtin(source) } diff --git a/gui/src/rust/ide/view/graph-editor/src/builtin/visualization/java_script/geoMap.js b/gui/src/rust/ide/view/graph-editor/src/builtin/visualization/java_script/geoMap.js index 6f185a83c0b7..5a6ab7574308 100644 --- a/gui/src/rust/ide/view/graph-editor/src/builtin/visualization/java_script/geoMap.js +++ b/gui/src/rust/ide/view/graph-editor/src/builtin/visualization/java_script/geoMap.js @@ -102,6 +102,15 @@ class GeoMapVisualization extends Visualization { this.initMapElement() this.initStyle() this.dataPoints = [] + this.setPreprocessorModule('Table.Main') + this.setPreprocessorCode(` + df -> case df of + Table.Table _ -> + columns = df.select ['label', 'latitude', 'longitude'] . columns + serialized = columns.map (c -> ['df_' + c.name, c.to_vector]) + Json.from_pairs serialized . to_text + _ -> df . to_json . to_text + `) } initMapElement() { @@ -136,37 +145,6 @@ class GeoMapVisualization extends Visualization { } onDataReceived(data) { - if (!this.isInit) { - // FIXME: This should be simplified when issue [#1167] - // (https://github.com/enso-org/ide/issues/1167) has been implemented. - // Use the previous version again: - // 'df -> case df of\n' + - // ' Table.Table _ ->\n' + - // " columns = df.select ['label', 'latitude', 'longitude'] . columns\n" + - // " serialized = columns.map (c -> ['df_' + c.name, c.to_vector])\n" + - // ' Json.from_pairs serialized . to_text\n' + - // ' _ -> df . to_json . to_text' - this.setPreprocessor( - 'df -> \n' + - ' get_cons_name : Any -> Text | Nothing\n' + - ' get_cons_name val =\n' + - ' meta_val = Meta.meta val\n' + - ' case meta_val of\n' + - ' Meta.Atom _ ->\n' + - ' cons = meta_val.constructor\n' + - ' Meta.Constructor cons . name \n' + - ' _ -> Nothing\n' + - " if (((get_cons_name df)) == 'Table').not then (df . to_json . to_text) else\n" + - " columns = df.select ['label', 'latitude', 'longitude'] . columns\n" + - " serialized = columns.map (c -> ['df_' + c.name, c.to_vector])\n" + - ' Json.from_pairs serialized . to_text' - ) - this.isInit = true - // We discard this data the first time. We will get another update with - // the correct data that has been transformed by the preprocessor. - return - } - let parsedData = data if (typeof data === 'string') { parsedData = JSON.parse(data) diff --git a/gui/src/rust/ide/view/graph-editor/src/builtin/visualization/java_script/scatterPlot.js b/gui/src/rust/ide/view/graph-editor/src/builtin/visualization/java_script/scatterPlot.js index 34040d3e31c8..e9a3aca03d32 100644 --- a/gui/src/rust/ide/view/graph-editor/src/builtin/visualization/java_script/scatterPlot.js +++ b/gui/src/rust/ide/view/graph-editor/src/builtin/visualization/java_script/scatterPlot.js @@ -18,12 +18,12 @@ const buttonsHeight = 25 /** * A d3.js ScatterPlot visualization. * - * To zoom use scrollwheel - * To select click and swipe with LMB - * To deselect click outside of selection with LMB - * To pan click and swipe with RMB - * To zoom out click "Fit all" or use key combination "ctrl/cmd+a" - * To zoom into selection click appropriate button or use key combination "ctrl/cmd+z" + * To zoom use scroll wheel. + * To select click and swipe with LMB. + * To deselect click outside of selection with LMB. + * To pan click and swipe with RMB. + * To zoom out click "Fit all" or use key combination "ctrl/cmd+a". + * To zoom into selection click appropriate button or use key combination "ctrl/cmd+z". * * Data format (json): * { diff --git a/gui/src/rust/ide/view/graph-editor/src/builtin/visualization/java_script/table.js b/gui/src/rust/ide/view/graph-editor/src/builtin/visualization/java_script/table.js index 0ba675a20acd..724a16a722cd 100644 --- a/gui/src/rust/ide/view/graph-editor/src/builtin/visualization/java_script/table.js +++ b/gui/src/rust/ide/view/graph-editor/src/builtin/visualization/java_script/table.js @@ -14,14 +14,21 @@ class TableVisualization extends Visualization { static inputType = 'Any' static label = 'Table' - onDataReceived(data) { - if (!this.isInit) { - this.setPreprocessor( - 'x -> (Json.from_pairs [["header", x.columns.map .name], ["data", x.columns.map .to_vector . map (x -> x.take_start 2000) ]]).to_text ' - ) - this.isInit = true - } + constructor(data) { + super(data) + this.setPreprocessorModule('Table.Main') + this.setPreprocessorCode(` + x -> case x of + Table.Table _ -> + header = ["header", x.columns.map .name] + data = ["data", x.columns.map .to_vector . map (x -> x.take_start 2000) ] + pairs = [header,data] + Json.from_pairs pairs . to_text + _ -> x . to_json . to_text + `) + } + onDataReceived(data) { function tableOf(content, level) { let open = '' return open + content + '
' diff --git a/gui/src/rust/ide/view/graph-editor/src/builtin/visualization/native/bubble_chart.rs b/gui/src/rust/ide/view/graph-editor/src/builtin/visualization/native/bubble_chart.rs index ea2c0020ead7..a9d7c03fb738 100644 --- a/gui/src/rust/ide/view/graph-editor/src/builtin/visualization/native/bubble_chart.rs +++ b/gui/src/rust/ide/view/graph-editor/src/builtin/visualization/native/bubble_chart.rs @@ -53,13 +53,13 @@ impl BubbleChartModel { fn receive_data(&self, data:&Data) -> Result<(),DataError> { let data_inner = match data { Data::Json {content} => content, - _ => todo!() // FIXME + _ => return Err(DataError::BinaryNotSupported), }; let data_inner:&serde_json::Value = data_inner; let data_inner: Rc>> = if let Ok(result) = serde_json::from_value(data_inner.clone()) { result } else { - return Err(DataError::InvalidDataType) + return Err(DataError::InvalidJsonText) }; // Avoid re-creating views, if we have already created some before. diff --git a/gui/src/rust/ide/view/graph-editor/src/builtin/visualization/native/error.rs b/gui/src/rust/ide/view/graph-editor/src/builtin/visualization/native/error.rs index 4921fc7c008e..353e69e5c995 100644 --- a/gui/src/rust/ide/view/graph-editor/src/builtin/visualization/native/error.rs +++ b/gui/src/rust/ide/view/graph-editor/src/builtin/visualization/native/error.rs @@ -2,9 +2,11 @@ use crate::prelude::*; +pub use crate::component::node::error::Kind; + use crate::component::visualization::*; use crate::component::visualization; -use crate::data::enso; +use crate::SharedHashMap; use enso_frp as frp; use ensogl::data::color; @@ -28,23 +30,33 @@ use serde::Serialize; const PADDING_TEXT:f32 = 10.0; /// The Error Visualization preprocessor. See also _Lazy Visualization_ section /// [here](http://dev.enso.org/docs/ide/product/visualizations.html). -pub const PREPROCESSOR:&str = r#"x -> - result = Ref.new "{ message: \"\"}" +pub const PREPROCESSOR_CODE:&str = r#" +x -> + result = Builtins.Ref.new "{ message: \"\"}" x.catch err-> message = err.to_display_text - Ref.put result ("{ \"kind\": \"Dataflow\", \"message\": " + message.to_json.to_text + "}") - Ref.get result + Builtins.Ref.put result ("{ \"kind\": \"Dataflow\", \"message\": " + message.to_json.to_text + "}") + Builtins.Ref.get result "#; +/// The context module for the `PREPROCESSOR_CODE`. See there. +pub const PREPROCESSOR_MODULE:&str = "Base.Main"; +/// Get preprocessor configuration for error visualization. +pub fn preprocessor() -> instance::PreprocessorConfiguration { + instance::PreprocessorConfiguration::new(PREPROCESSOR_CODE,PREPROCESSOR_MODULE) +} + +/// Get metadata description for error visualization. +pub fn metadata() -> Metadata { + let preprocessor = preprocessor(); + Metadata {preprocessor} +} // ============= // === Input === // ============= -pub use crate::component::node::error::Kind; -use crate::SharedHashMap; - /// The input for Error Visualization. #[allow(missing_docs)] #[derive(Clone,Debug,Deserialize,Serialize)] @@ -108,8 +120,7 @@ impl Error { }); } - frp.preprocessor_change.emit(enso::Code::from(PREPROCESSOR)); - + frp.preprocessor_change.emit(preprocessor()); self } @@ -183,14 +194,14 @@ impl Model { } fn receive_data(&self, data:&Data) -> Result<(),DataError> { - iprintln!("Receive data {data:?}"); - if let Data::Json {content} = data { - let input_result = serde_json::from_value(content.deref().clone()); - let input:Input = input_result.map_err(|_| DataError::InvalidDataType)?; - self.set_data(&input); - Ok(()) - } else { - Err(DataError::InvalidDataType) + match data { + Data::Json {content} => { + let input_result = serde_json::from_value(content.deref().clone()); + let input:Input = input_result.map_err(|_| DataError::InvalidDataType)?; + self.set_data(&input); + Ok(()) + } + Data::Binary => Err(DataError::BinaryNotSupported) } } diff --git a/gui/src/rust/ide/view/graph-editor/src/component/visualization/container.rs b/gui/src/rust/ide/view/graph-editor/src/component/visualization/container.rs index c3cef9d7f003..d782c60e0781 100644 --- a/gui/src/rust/ide/view/graph-editor/src/component/visualization/container.rs +++ b/gui/src/rust/ide/view/graph-editor/src/component/visualization/container.rs @@ -13,8 +13,8 @@ mod visualization_chooser; use crate::prelude::*; -use crate::data::enso; use crate::visualization; +use crate::component::visualization::instance::PreprocessorConfiguration; use action_bar::ActionBar; use enso_frp as frp; @@ -33,7 +33,6 @@ use ensogl_theme as theme; - // ================= // === Constants === // ================= @@ -166,7 +165,7 @@ ensogl::define_endpoints! { } Output { - preprocessor (enso::Code), + preprocessor (PreprocessorConfiguration), visualisation (Option), size (Vector2), is_selected (bool), @@ -399,14 +398,14 @@ impl ContainerModel { } fn set_visualization - (&self, visualization:visualization::Instance, preprocessor:&frp::Any) { + (&self, visualization:visualization::Instance, preprocessor:&frp::Any) { let size = self.size.get(); visualization.set_size.emit(size); frp::new_network! { vis_frp_connection // We need an additional "copy" node here. We create a new network to manage lifetime of // connection between `visualization.on_preprocessor_change` and `preprocessor`. // However, doing simple `preprocessor <+ visualization.on_preprocessor_change` will not - // create any node in this network, so in fact ot won't manage the connection. + // create any node in this network, so in fact it won't manage the connection. vis_preprocessor_change <- visualization.on_preprocessor_change.map(|x| x.clone()); preprocessor <+ vis_preprocessor_change; } @@ -546,7 +545,7 @@ impl Container { action_bar.set_selected_visualization.emit(path); }, Err(err) => { - warning!(logger,"Failed to instantiate visualisation: {err:?}"); + warning!(logger,"Failed to instantiate visualization: {err:?}"); }, }; } diff --git a/gui/src/rust/ide/view/graph-editor/src/component/visualization/data.rs b/gui/src/rust/ide/view/graph-editor/src/component/visualization/data.rs index 83d5d8268fdf..e2330f8d6f8c 100644 --- a/gui/src/rust/ide/view/graph-editor/src/component/visualization/data.rs +++ b/gui/src/rust/ide/view/graph-editor/src/component/visualization/data.rs @@ -105,8 +105,12 @@ impl From for Data { /// violates some other assumption of the visualization. #[derive(Copy,Clone,Debug)] pub enum DataError { - /// Indicates that that the provided data type does not match the expected data type. + /// Visualization received a binary data package, which is currently not supported. + BinaryNotSupported, + /// Indicates that that the provided data type does not match the expected data format. InvalidDataType, + /// Received text data is not valid JSON while it is required. + InvalidJsonText, /// The data caused an error in the computation of the visualization. InternalComputationError, } diff --git a/gui/src/rust/ide/view/graph-editor/src/component/visualization/foreign/java_script/binding.rs b/gui/src/rust/ide/view/graph-editor/src/component/visualization/foreign/java_script/binding.rs index 0677146f68e4..7a91b2af8154 100644 --- a/gui/src/rust/ide/view/graph-editor/src/component/visualization/foreign/java_script/binding.rs +++ b/gui/src/rust/ide/view/graph-editor/src/component/visualization/foreign/java_script/binding.rs @@ -2,7 +2,8 @@ use crate::prelude::*; -use super::PreprocessorCallback; +use crate::component::visualization::foreign::java_script::PreprocessorCallback; +use crate::component::visualization::instance::PreprocessorConfiguration; use ensogl::display::DomSymbol; use fmt::Formatter; @@ -30,6 +31,7 @@ extern "C" { fn __Visualization__() -> JsValue; #[allow(unsafe_code)] + #[wasm_bindgen(extends = js_sys::Object)] pub type Visualization; #[allow(unsafe_code)] @@ -37,8 +39,8 @@ extern "C" { fn new(init:JsConsArgs) -> Visualization; #[allow(unsafe_code)] - #[wasm_bindgen(method)] - fn setPreprocessor(this:&Visualization, code:String); + #[wasm_bindgen(catch, js_name = __emitPreprocessorChange__, method)] + pub fn emitPreprocessorChange(this:&Visualization) -> Result<(),JsValue>; } /// Provides reference to the visualizations JavaScript base class. @@ -85,8 +87,9 @@ impl JsConsArgs { } /// Helper method to emit an preprocessor change event from the visualisation. - pub fn emit_preprocessor_change(&self, code:String){ - let closure = &self.set_preprocessor; - (*closure)(code); + pub fn emit_preprocessor_change(&self, code:Option, module:Option){ + let closure = &self.set_preprocessor; + let preprocessor_config = PreprocessorConfiguration::from_options(code,module); + (*closure)(preprocessor_config); } } diff --git a/gui/src/rust/ide/view/graph-editor/src/component/visualization/foreign/java_script/definition.rs b/gui/src/rust/ide/view/graph-editor/src/component/visualization/foreign/java_script/definition.rs index 0e8a492422c0..d583d2ecdf21 100644 --- a/gui/src/rust/ide/view/graph-editor/src/component/visualization/foreign/java_script/definition.rs +++ b/gui/src/rust/ide/view/graph-editor/src/component/visualization/foreign/java_script/definition.rs @@ -1,100 +1,8 @@ //! Definition of visualization JavaScript API. //! -//! Visualizations can be defined as a JavaScript function which returns a class of a shape -//! specified below. Consider the following definition: -//! -//! ```javascript -//! console.log("Hi, this definition is being registered now!") -//! -//! return class BubbleVisualization extends Visualization { -//! static inputType = "Any" -//! -//! onDataReceived(data) { -//! const xmlns = "http://www.w3.org/2000/svg"; -//! while (this.dom.firstChild) { -//! this.dom.removeChild(this.dom.lastChild); -//! } -//! const width = this.dom.getAttributeNS(null, "width"); -//! const height = this.dom.getAttributeNS(null, "height"); -//! const svgElem = document.createElementNS(xmlns, "svg"); -//! svgElem.setAttributeNS(null, "id" , "vis-svg"); -//! svgElem.setAttributeNS(null, "viewBox", "0 0 " + width + " " + height); -//! svgElem.setAttributeNS(null, "width" , "100%"); -//! svgElem.setAttributeNS(null, "height" , "100%"); -//! this.dom.appendChild(svgElem); -//! data.forEach(data => { -//! const bubble = document.createElementNS(xmlns,"circle"); -//! bubble.setAttributeNS(null,"stroke", "black"); -//! bubble.setAttributeNS(null,"fill" , "red"); -//! bubble.setAttributeNS(null,"r" , data[2]); -//! bubble.setAttributeNS(null,"cx" , data[0]); -//! bubble.setAttributeNS(null,"cy" , data[1]); -//! svgElem.appendChild(bubble); -//! }); -//! } -//! -//! setSize(size) { -//! this.dom.setAttributeNS(null, "width", size[0]); -//! this.dom.setAttributeNS(null, "height", size[1]); -//! } -//! } -//! ``` -//! -//! In particular: -//! -//! - [Required] **Source code** -//! Visualization definition has to be a valid body of JavaScript function which returns a class -//! definition. Instances of that class will be considered separate visualizations. You are -//! allowed to use global variables / global state across visualizations of the same type, but you -//! are highly advised not to do so. -//! -//! - [Required] **`Visualization` superclass** -//! The class returned by the definition function should extend the predefined `Visualization` -//! class. Classes which do not extend it, will not be registered as visualizations. The -//! superclass defines a default constructor and a set of utils: -//! - The `setPreprocessor(code)` method allowing setting an Enso code which will be evaluated on -//! server-side before sending data to visualization. -//! - The `dom` field, which will be initialized in the constructor to the DOM symbol used to host -//! the visualization content. You are free to modify the DOM element, including adding other -//! elements as its children. -//! -//! - [Optional] **Field `label`** -//! The static field `label` is an user-facing name used to identify the visualization. You are -//! not allowed to define several visualizations of the same name in the same Enso library. In -//! case the field is missing, the name will be inferred from the class name by splitting the -//! camel-case name into chunks and converting them to lowercase string. -//! -//! - [Optional] **Field `inputType`** -//! The static field `inputType` is used to determine which Enso data types this visualization -//! can be used for. Its value should be a valid Enso type, like "String | Int". In case the field -//! is an empty string or it is missing, it will default to "Any", which is a type containing all -//! other types. It is a rare case when you want to define a visualization which is able to work -//! with just any data type, so you are highly advised to provide the type definition. -//! -//! - [Optional] **Field `inputFormat`** -//! The static field `inputFormat` is used to determine what format the data should be provided -//! to the `onDataReceived` function. Currently, the only valid option is "json", but it will be -//! possible to set it to "binary" in the future. In the later case, it is up to the visualization -//! author to manage the binary stream received from the server. -//! -//! - [Optional] **Constructor** -//! The visualization will be instantiated by providing the constructor with a configuration -//! object. The shape of the configuration object is not part of the public API and can change -//! between releases of this library. You have to pass it unchanged to the superclass constructor. -//! -//! - [Optional] **Function `onDataReceived`** -//! The `onDataReceived(data)` method is called on every new data chunk received from the server. -//! Note that the visualization will receive the "full data" if you are not using the -//! `setPreprocessor` method. -//! -//! - [Optional] **Function `setSize`** -//! The `setSize(size)` method is called on every size change of the visualization. You should not -//! draw outside of the provided area, however, if you do so, it will be clipped to the provided -//! area automatically. The `size` parameter contains two fields `width` and `height` expressed in -//! pixels. - -// TODO: Connect the `setPreprocessor` method on Rust side. - +//! For details of the API please see: +//! * docstrings in the `visualization.js` file; +//! * [visualization documentation](https://dev.enso.org/docs/ide/product/visualizations.html). // FIXME: Can we simplify the above definition so its more minimal, yet functional? @@ -103,17 +11,17 @@ use crate::prelude::*; use crate::component::visualization::InstantiationError; use crate::component::visualization::InstantiationResult; use crate::component::visualization; -use crate::data::enso; use super::binding; use super::instance::Instance; use ensogl::display::Scene; use ensogl::system::web::JsValue; +use fmt::Formatter; use js_sys::JsString; use js_sys; -use wasm_bindgen::JsCast; -use fmt::Formatter; use std::str::FromStr; +use wasm_bindgen::JsCast; + // ================= @@ -150,7 +58,7 @@ pub struct Definition { impl Definition { /// Create a visualization source from piece of JS source code. Signature needs to be inferred. - pub fn new (library:impl Into, source:impl AsRef) -> Result { + pub fn new(project:visualization::path::Project, source:impl AsRef) -> Result { let source = source.as_ref(); let source = source; let context = JsValue::NULL; @@ -158,19 +66,23 @@ impl Definition { let js_class = binding::js_class(); let class = function.call1(&context,&js_class).map_err(Error::InvalidFunction)?; - let library = library.into(); let input_type = try_str_field(&class,field::INPUT_TYPE).unwrap_or_default(); let input_format = try_str_field(&class,field::INPUT_FORMAT).unwrap_or_default(); let input_format = visualization::data::Format::from_str(&input_format).unwrap_or_default(); let label = label(&class)?; - let path = visualization::Path::new(library,label); + let path = visualization::Path::new(project,label); let signature = visualization::Signature::new(path,input_type,input_format); Ok(Self{class,signature}) } + /// Create a definition of visualization that is built into the IDE. + pub fn new_builtin(source:impl AsRef) -> Result { + Self::new(visualization::path::Project::Builtin,source) + } + fn new_instance(&self, scene:&Scene) -> InstantiationResult { let instance = Instance::new(&self.class, scene) .map_err(InstantiationError::ConstructorError)?; diff --git a/gui/src/rust/ide/view/graph-editor/src/component/visualization/foreign/java_script/instance.rs b/gui/src/rust/ide/view/graph-editor/src/component/visualization/foreign/java_script/instance.rs index 0c64ce8cff7f..7a5214e3edb4 100644 --- a/gui/src/rust/ide/view/graph-editor/src/component/visualization/foreign/java_script/instance.rs +++ b/gui/src/rust/ide/view/graph-editor/src/component/visualization/foreign/java_script/instance.rs @@ -10,6 +10,7 @@ use crate::prelude::*; use crate::component::visualization::*; +use crate::component::visualization::instance::PreprocessorConfiguration; use crate::component::visualization::java_script::binding::JsConsArgs; use crate::component::visualization::java_script::method; use crate::component::visualization; @@ -76,7 +77,7 @@ pub type Result = result::Result; // ===================== /// Helper type for the callback used to set the preprocessor code. -pub trait PreprocessorCallback = Fn(String); +pub trait PreprocessorCallback = Fn(PreprocessorConfiguration); /// Internal helper type to store the preprocessor callback. type PreprocessorCallbackCell = Rc>>>; @@ -91,7 +92,8 @@ pub struct InstanceModel { pub logger : Logger, on_data_received : Rc>, set_size : Rc>, - object : Rc, + #[derivative(Debug="ignore")] + object : Rc, #[derivative(Debug="ignore")] preprocessor_change : PreprocessorCallbackCell, } @@ -131,16 +133,16 @@ impl InstanceModel { () -> (PreprocessorCallbackCell,impl PreprocessorCallback) { let closure_cell = PreprocessorCallbackCell::default(); let weak_closure_cell = Rc::downgrade(&closure_cell); - let closure = move |s:String| { + let closure = move |preprocessor_config| { if let Some(callback) = weak_closure_cell.upgrade() { - callback.borrow().map_ref(|f|f(s)); + callback.borrow().map_ref(|f|f(preprocessor_config)); } }; (closure_cell,closure) } fn instantiate_class_with_args(class:&JsValue, args:JsConsArgs) - -> result::Result { + -> result::Result { let js_new = js_sys::Function::new_with_args("cls,arg", "return new cls(arg)"); let context = JsValue::NULL; let object = js_new.call2(&context,&class,&args.into()) @@ -148,7 +150,7 @@ impl InstanceModel { if !object.is_object() { return Err(Error::ValueIsNotAnObject { object } ) } - let object:js_sys::Object = object.into(); + let object:java_script::binding::Visualization = object.into(); Ok(object) } @@ -159,9 +161,9 @@ impl InstanceModel { let (preprocessor_change,closure) = Self::preprocessor_change_callback(); let init_data = JsConsArgs::new(root_node.clone_ref(), closure); let object = Self::instantiate_class_with_args(class,init_data)?; - let on_data_received = get_method(&object,method::ON_DATA_RECEIVED).ok(); + let on_data_received = get_method(object.as_ref(),method::ON_DATA_RECEIVED).ok(); let on_data_received = Rc::new(on_data_received); - let set_size = get_method(&object,method::SET_SIZE).ok(); + let set_size = get_method(&object.as_ref(),method::SET_SIZE).ok(); let set_size = Rc::new(set_size); let object = Rc::new(object); Ok(InstanceModel{object,on_data_received,set_size,root_node,logger,preprocessor_change}) @@ -180,20 +182,25 @@ impl InstanceModel { self.root_node.set_size(size); } - fn receive_data(&self, data:&Data) -> result::Result<(),DataError> { - let data_json = match data { - Data::Json {content} => content, - _ => todo!() // FIXME - }; - let data_json:&serde_json::Value = data_json.deref(); - let data_js = match JsValue::from_serde(data_json) { - Ok(value) => value, - Err(_) => return Err(DataError::InvalidDataType), - }; - self.try_call1(&self.on_data_received, &data_js) - .map_err(|_| DataError::InternalComputationError)?; - Ok(()) - } + fn receive_data(&self, data:&Data) -> result::Result<(),DataError> { + let data_json = match data { + Data::Json {content} => content, + _ => return Err(DataError::BinaryNotSupported), + }; + let data_json:&serde_json::Value = data_json.deref(); + let data_js = match JsValue::from_serde(data_json) { + Ok(value) => value, + Err(_) => return Err(DataError::InvalidDataType), + }; + self.try_call1(&self.on_data_received, &data_js) + .map_err(|_| DataError::InternalComputationError)?; + Ok(()) + } + + /// Prompt visualization JS object to emit preprocessor change with its currently desired state. + pub fn update_preprocessor(&self) -> result::Result<(),JsValue> { + self.object.emitPreprocessorChange() + } /// Helper method to call methods on the wrapped javascript object. fn try_call1(&self, method:&Option, arg:&JsValue) @@ -231,7 +238,7 @@ impl Instance { let frp = visualization::instance::Frp::new(&network); let model = InstanceModel::from_class(class,scene)?; model.set_dom_layer(&scene.dom.layers.back); - Ok(Instance{model,frp,network}.init_frp(&scene).inti_preprocessor_change_callback()) + Ok(Instance{model,frp,network}.init_frp(&scene).init_preprocessor_change_callback()) } fn init_frp(self, scene:&Scene) -> Self { @@ -250,12 +257,20 @@ impl Instance { self } - fn inti_preprocessor_change_callback(self) -> Self { + fn init_preprocessor_change_callback(self) -> Self { // FIXME Does it leak memory? To be checked. - let change = &self.frp.preprocessor_change; - let callback = f!((s:String) change.emit(&s.into())); + let change = self.frp.preprocessor_change.clone_ref(); + let callback = move |preprocessor_config| { + change.emit(preprocessor_config) + }; let callback = Box::new(callback); self.model.preprocessor_change.borrow_mut().replace(callback); + if let Err(js_error) = self.model.update_preprocessor() { + use enso_frp::web::js_to_string; + let logger = self.model.logger.clone(); + error!(logger,"Failed to trigger initial preprocessor update from JS: \ + {js_to_string(js_error)}"); + } self } diff --git a/gui/src/rust/ide/view/graph-editor/src/component/visualization/foreign/java_script/visualization.js b/gui/src/rust/ide/view/graph-editor/src/component/visualization/foreign/java_script/visualization.js index 009408b66ad1..e64cd77f369e 100644 --- a/gui/src/rust/ide/view/graph-editor/src/component/visualization/foreign/java_script/visualization.js +++ b/gui/src/rust/ide/view/graph-editor/src/component/visualization/foreign/java_script/visualization.js @@ -1,10 +1,130 @@ export class Visualization { + /** + * Class constructor. + * + * The argument is IDE's implementation detail. + * + * When subclassing and using custom constructor, the argument must be passed + * to this constructor and visualization implementation is not allowed to + * perform any other operation on it. + * + * @param api an internal IDE structure. Should be passed intact by a subclass + * constructor (if present). + */ constructor(api) { + // These go before `api` assignment so the `undefined` is not emitted to IDE. + // First we will give deriving type a chance to overwrite them, then IDE will + // invoke `__emitPreprocessorChange__()` on this. + this.__preprocessorCode__ = null + this.__preprocessorModule__ = null + this.dom = api.root() this.__api__ = api } - setPreprocessor(code) { - this.__api__.emit_preprocessor_change(code) + + /** + * Notify IDE that preprocessor settings have changed. + * @private + */ + __emitPreprocessorChange__() { + this.__api__.emit_preprocessor_change( + this.__preprocessorCode__, + this.__preprocessorModule__ + ) + } + + /** + * Get the current preprocessor code. See {@link setPreprocessorCode} for + * more information about purpose of setting preprocessor code. + * + * @returns {string} Preprocessor code or `null` if no code was set. + */ + getPreprocessorCode() { + return this.__preprocessorCode__ + } + + /** + * Set new preprocessor code. + * + * When visualization is attached to a node, each time a new value is produced from node, + * the preprocessor shall be invoked with it. Result such call shall be serialized and + * transported to visualization by invoking onDataReceived(data) method. + * + * Typically the preprocessor is a lambda, like the example below: + * + * `x -> x.to_default_visualization_data` + * + * + * The code by default runs in the context of the current project's `Main` module. + * If other context is needed (e.g. due to required import or other module-specific + * context dependency), the {@link setPreprocessorModule} should be used to provide + * the module's name. + * + * Please refer to [documentation]{@link https://dev.enso.org/docs/ide/product/visualizations.html#lazy-visualizations} + * for details. + * + * @param {string} code text code in Enso. It must be invokable with one argument and return + * JSON-compatible result. For example: + *
x -> x.to_default_visualization_data
+ */ + setPreprocessorCode(code) { + if (code !== this.__preprocessorCode__) { + this.__preprocessorCode__ = code + this.__emitPreprocessorChange__() + } + } + + /** + * Get the current preprocessor's context module. + * + * See the [setter documentation]{@link setPreprocessorModule} for more information. + * + * @returns {string} Qualified name to preprocessor's context module. + */ + getPreprocessorModule() { + return this.__preprocessorModule__ + } + + /** + * Set preprocessor's context module. + * + * [Preprocessor code]{@link setPreprocessorCode} is executed in the context of + * certain Enso module. This decides what symbols are visible and available to + * preprocessor, as everything that preprocessor uses must defined or imported + * in the context module. + * + * If never set, Engine will use the current project's `Main` module as the context. + * + * @param module + */ + setPreprocessorModule(module) { + if (module !== this.__preprocessorModule__) { + this.__preprocessorModule__ = module + this.__emitPreprocessorChange__() + } else { + console.error('skipping, as', module, ' === ', this.__preprocessorModule__) + } + } + + /** + * Set both preprocessor's code and context module. + * + * This is like calling both {@link setPreprocessorModule} and + * {@link setPreprocessorCode}, however may be more efficient, as it will emit + * only one update request. + * + * During the visualization construction phase no partial updates are emitted, + * so using this method gives no additional benefit. + * + * @param code preprocessor code to be set. + * @param module context module for the preprocessor execution. + */ + setPreprocessor(code, module) { + if (code !== this.__preprocessorCode__ || code !== this.__preprocessorModule__) { + this.__preprocessorCode__ = code + this.__preprocessorModule__ = module + this.__emitPreprocessorChange__() + } } } diff --git a/gui/src/rust/ide/view/graph-editor/src/component/visualization/instance.rs b/gui/src/rust/ide/view/graph-editor/src/component/visualization/instance.rs index db652aa5b468..e482cbd8ce43 100644 --- a/gui/src/rust/ide/view/graph-editor/src/component/visualization/instance.rs +++ b/gui/src/rust/ide/view/graph-editor/src/component/visualization/instance.rs @@ -20,6 +20,84 @@ pub const DEFAULT_VISUALIZATION_EXPRESSION:&str = "x -> x.to_default_visualizati +// ==================== +// === Preprocessor === +// ==================== + +// === ContextModule === + +/// Designation of the module to be used as a context for preprocessor evaluation. +#[derive(Clone,CloneRef,Debug)] +pub enum ContextModule { + /// Current project's `Main` module. + ProjectMain, + /// Specific module of known name. + Specific(enso::Module) +} + +impl Default for ContextModule { + fn default() -> Self { + ContextModule::ProjectMain + } +} + +impl ContextModule { + /// Create a context from optional string with module's type. + /// + /// If there is no explicit module's type provided, the default (project's main) will be used. + pub fn new(module_type:impl Into) -> Self { + Self::Specific(module_type.into()) + } +} + + +// === PreprocessorConfiguration === + +/// Information on how the preprocessor should be set up for the visualization. +#[derive(Clone,CloneRef,Debug)] +pub struct PreprocessorConfiguration { + /// The code of the preprocessor. Should be a lambda that transforms node value into whatever + /// that visualizations expect. + pub code : enso::Code, + /// The module that provides context for `code` evaluation. + pub module : ContextModule, +} + +impl PreprocessorConfiguration { + /// Like `new` but arguments are optional. If `None` is given, default value will be used. + pub fn from_options + (code:Option>, module:Option>) -> Self { + let mut ret = Self::default(); + if let Some(code) = code { + ret.code = code.into() + } + if let Some(module) = module { + ret.module = ContextModule::Specific(module.into()) + } + ret + } + + /// Create a configuration that runs the given code in the context of the given module. + pub fn new + (code:impl Into, module:impl Into) -> PreprocessorConfiguration { + PreprocessorConfiguration { + module : ContextModule::Specific(module.into()), + code : code.into(), + } + } +} + +impl Default for PreprocessorConfiguration { + fn default() -> Self { + Self { + code : DEFAULT_VISUALIZATION_EXPRESSION.into(), + module : default(), + } + } +} + + + // =========== // === FRP === // =========== @@ -48,7 +126,7 @@ pub struct Frp { #[shrinkwrap(main_field)] pub inputs : FrpInputs, - pub on_preprocessor_change : frp::Sampler, + pub on_preprocessor_change : frp::Sampler, pub on_data_receive_error : frp::Stream>, pub is_active : frp::Stream, @@ -59,7 +137,7 @@ pub struct Frp { /// a function called on the Engine side before sending data to IDE, allowing us to do some /// compression or filtering for the best performance. See also _Lazy Visualization_ section /// [here](http://dev.enso.org/docs/ide/product/visualizations.html). - pub preprocessor_change : frp::Source, + pub preprocessor_change : frp::Source, } impl FrpInputs { @@ -85,7 +163,7 @@ impl Frp { def data_receive_error = source(); is_active <- bool(&inputs.deactivate,&inputs.activate); }; - preprocessor_change.emit(enso::Code::new(DEFAULT_VISUALIZATION_EXPRESSION)); + preprocessor_change.emit(PreprocessorConfiguration::default()); let on_data_receive_error = data_receive_error.clone_ref().into(); Self {on_preprocessor_change,on_data_receive_error,is_active,preprocessor_change,inputs ,data_receive_error} diff --git a/gui/src/rust/ide/view/graph-editor/src/component/visualization/metadata.rs b/gui/src/rust/ide/view/graph-editor/src/component/visualization/metadata.rs index 33b3b7401b81..e97ba81c7a49 100644 --- a/gui/src/rust/ide/view/graph-editor/src/component/visualization/metadata.rs +++ b/gui/src/rust/ide/view/graph-editor/src/component/visualization/metadata.rs @@ -2,7 +2,7 @@ use crate::prelude::*; -use crate::data::enso; +use crate::component::visualization::instance::PreprocessorConfiguration; /// Description of the visualization state, emitted with visualization_enabled event in GraphEditor. #[derive(Clone,Debug,Default)] @@ -10,5 +10,5 @@ pub struct Metadata { /// An Enso lambda, called on the Engine side before sending data to IDE, allowing us to do some /// compression or filtering for the best performance. See also _Lazy Visualization_ section /// [here](http://dev.enso.org/docs/ide/product/visualizations.html). - pub preprocessor: enso::Code, + pub preprocessor:PreprocessorConfiguration, } diff --git a/gui/src/rust/ide/view/graph-editor/src/component/visualization/path.rs b/gui/src/rust/ide/view/graph-editor/src/component/visualization/path.rs index 7a1aa06ae23d..6b89ab411db1 100644 --- a/gui/src/rust/ide/view/graph-editor/src/component/visualization/path.rs +++ b/gui/src/rust/ide/view/graph-editor/src/component/visualization/path.rs @@ -16,6 +16,18 @@ im_string_newtype!{ Name } +/// Identifier to the project owning the visualizaiton. +#[derive(Clone,CloneRef,Debug,Eq,Hash,PartialEq)] +pub enum Project { + /// Temporary placeholder for the visualizations embedded in the IDE. + /// Eventually will be replaced with Standard Library. + Builtin, + /// The current project (i.e. the project that user has open in the project manager). + CurrentProject, + /// An external library (i.e. the dependency of the current project). + Library(enso::LibraryName), +} + // ============ @@ -27,22 +39,20 @@ im_string_newtype!{ #[derive(Clone,CloneRef,Debug,Eq,Hash,PartialEq)] #[allow(missing_docs)] pub struct Path { - pub library : enso::LibraryName, + pub project : Project, pub name : Name, } impl Path { /// Constructor. - pub fn new(library:impl Into, name:impl Into) -> Self { - let library = library.into(); + pub fn new(project:Project, name:impl Into) -> Self { let name = name.into(); - Self {library,name} + Self {project,name} } /// Constructor for builtin visualizations. pub fn builtin(name:impl Into) -> Self { - let library = enso::builtin_library(); - Self::new(library,name) + Self::new(Project::Builtin,name) } } diff --git a/gui/src/rust/ide/view/graph-editor/src/data.rs b/gui/src/rust/ide/view/graph-editor/src/data.rs index 45c256b389c8..ce9f92912b78 100644 --- a/gui/src/rust/ide/view/graph-editor/src/data.rs +++ b/gui/src/rust/ide/view/graph-editor/src/data.rs @@ -16,6 +16,9 @@ pub mod enso { /// The Enso type representation. Can be a complex type, like `String|Int`. Type, + + /// The Enso module represented as qualified path, like `Project.Data.Vector`. + Module, } impl Type { @@ -24,9 +27,4 @@ pub mod enso { "Any".into() } } - - /// Builtin library name. For internal usage only. - pub fn builtin_library() -> LibraryName { - "builtin".into() - } } diff --git a/gui/src/rust/ide/view/graph-editor/src/lib.rs b/gui/src/rust/ide/view/graph-editor/src/lib.rs index 52d1ba3cb61e..b524e495b324 100644 --- a/gui/src/rust/ide/view/graph-editor/src/lib.rs +++ b/gui/src/rust/ide/view/graph-editor/src/lib.rs @@ -36,6 +36,7 @@ pub mod data; use crate::component::node; use crate::component::visualization; +use crate::component::visualization::instance::PreprocessorConfiguration; use crate::component::visualization::MockDataGenerator3D; use crate::component::type_coloring; @@ -516,7 +517,7 @@ ensogl::define_endpoints! { visualization_enabled (NodeId,visualization::Metadata), visualization_disabled (NodeId), visualization_enable_fullscreen (NodeId), - visualization_preprocessor_changed ((NodeId,data::enso::Code)), + visualization_preprocessor_changed ((NodeId,PreprocessorConfiguration)), visualization_registry_reload_requested (), on_visualization_select (Switch), @@ -1087,13 +1088,14 @@ impl GraphEditorModelWithNetwork { selected <- vis_is_selected.on_true(); deselected <- vis_is_selected.on_false(); output.source.visualization_preprocessor_changed <+ - node.model.visualization.frp.preprocessor.map(move |code| (node_id,code.clone())); + node.model.visualization.frp.preprocessor.map(move |preprocessor| + (node_id,preprocessor.clone())); output.source.on_visualization_select <+ selected.constant(Switch::On(node_id)); output.source.on_visualization_select <+ deselected.constant(Switch::Off(node_id)); metadata <- any(...); - metadata <+ node.model.visualization.frp.preprocessor.map(move |code| { - let preprocessor = code.clone(); + metadata <+ node.model.visualization.frp.preprocessor.map(move |preprocessor| { + let preprocessor = preprocessor.clone(); visualization::Metadata{preprocessor} }); // Ensure the graph editor knows about internal changes to the visualisation. If the diff --git a/gui/src/rust/ide/view/src/debug_scenes/visualization.rs b/gui/src/rust/ide/view/src/debug_scenes/visualization.rs index c325e6c83ac6..ea1b70eb9cfb 100644 --- a/gui/src/rust/ide/view/src/debug_scenes/visualization.rs +++ b/gui/src/rust/ide/view/src/debug_scenes/visualization.rs @@ -3,7 +3,6 @@ use crate::graph_editor::component::visualization::Data; use crate::graph_editor::component::visualization; use crate::graph_editor::component::visualization::Registry; -use crate::graph_editor::data::enso; use ensogl::application::Application; use ensogl::display::navigation::navigator::Navigator; @@ -73,7 +72,7 @@ fn constructor_graph() -> visualization::java_script::Definition { return Graph "#; - visualization::java_script::Definition::new(enso::builtin_library(),source).unwrap() + visualization::java_script::Definition::new_builtin(source).unwrap() } #[wasm_bindgen] diff --git a/gui/src/rust/ide/view/src/documentation.rs b/gui/src/rust/ide/view/src/documentation.rs index a66b92046a1e..4a7db9453445 100644 --- a/gui/src/rust/ide/view/src/documentation.rs +++ b/gui/src/rust/ide/view/src/documentation.rs @@ -205,7 +205,8 @@ impl Model { return Err(visualization::DataError::InternalComputationError); } } - _ => return Err(visualization::DataError::InvalidDataType), + visualization::Data::Binary => + return Err(visualization::DataError::BinaryNotSupported), }; self.display_doc(&string, InputFormat::Docstring); Ok(())