diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 439214d5ba43..4415d1a70954 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -39,3 +39,15 @@ Cargo.toml # This section should be amended once the engine moves to /app/engine /distribution/lib/ @jdunkerley @radeusgd /std-bits/ @jdunkerley @radeusgd + +# Cloud Dashboard & Authentication +/app/ide-desktop/dashboard @PabloBuchu @indiv0 @somebody1234 +/app/ide-desktop/content @PabloBuchu @indiv0 @somebody1234 +/app/ide-desktop/client @PabloBuchu @indiv0 @somebody1234 +/app/ide-desktop/types @PabloBuchu @indiv0 @somebody1234 +/app/ide-desktop/common @PabloBuchu @indiv0 @somebody1234 + +# Eslint configuration +/app/ide-desktop/esbuild-plugin-copy-directories @somebody1234 +/app/ide-desktop/eslint.config.js @somebody1234 +/app/ide-desktop/utils.ts @somebody1234 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1207e6b630d2..e00e9db3f06c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -380,7 +380,11 @@ - [Added support for Date/Time columns in the Postgres backend and added `year`/`month`/`day` operations to Table columns.][6153] - [`Text.split` can now take a vector of delimiters.][6156] +- [Add `has_warnings`, `remove_warnings` and `throw_on_warning` extension + methods.][6176] - [Implemented `Table.union` for the Database backend.][6204] +- [Array & Vector have the same methods & behavior][6218] +- [Implemented `Table.split` and `Table.tokenize` for in-memory tables.][6233] [debug-shortcuts]: https://github.com/enso-org/enso/blob/develop/app/gui/docs/product/shortcuts.md#debug @@ -574,8 +578,11 @@ [6150]: https://github.com/enso-org/enso/pull/6150 [6153]: https://github.com/enso-org/enso/pull/6153 [6156]: https://github.com/enso-org/enso/pull/6156 +[6176]: https://github.com/enso-org/enso/pull/6176 [6204]: https://github.com/enso-org/enso/pull/6204 [6077]: https://github.com/enso-org/enso/pull/6077 +[6218]: https://github.com/enso-org/enso/pull/6218 +[6233]: https://github.com/enso-org/enso/pull/6233 #### Enso Compiler @@ -680,6 +687,7 @@ - [Ensure calls involving warnings remain instrumented][6067] - [One can define lazy atom fields][6151] - [Replace IOContexts with Execution Environment and generic Context][6171] +- [Vector.sort handles incomparable types][5998] [3227]: https://github.com/enso-org/enso/pull/3227 [3248]: https://github.com/enso-org/enso/pull/3248 @@ -785,6 +793,7 @@ [6067]: https://github.com/enso-org/enso/pull/6067 [6151]: https://github.com/enso-org/enso/pull/6151 [6171]: https://github.com/enso-org/enso/pull/6171 +[5998]: https://github.com/enso-org/enso/pull/5998 # Enso 2.0.0-alpha.18 (2021-10-12) diff --git a/Cargo.lock b/Cargo.lock index 057c5801aff4..d0788c952a25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2674,6 +2674,8 @@ dependencies = [ "num-traits", "num_enum", "ordered-float", + "rand 0.8.5", + "rand_chacha 0.3.1", "rustc-hash", "semver 1.0.16", "serde", diff --git a/README.md b/README.md index 33f54f120cfd..03ed0315bf85 100644 --- a/README.md +++ b/README.md @@ -187,9 +187,8 @@ always trust the results you get. If you want to start _using_ Enso, please see the download links in the [getting started](#getting-started) section above. Alternatively, you can get -the IDE [here](https://github.com/enso-org/ide/releases) and the language itself -[here](https://github.com/enso-org/enso/releases). This section is intended for -people interested in contributing to the development of Enso. +the IDE [here](https://github.com/enso-org/enso/releases). This section is +intended for people interested in contributing to the development of Enso. Enso is a community-driven open source project which is, and will always be, open and free to use. Join us, help us to build it, and spread the word! diff --git a/app/gui/src/presenter/graph.rs b/app/gui/src/presenter/graph.rs index ef483952fb26..f8cad3e1e11c 100644 --- a/app/gui/src/presenter/graph.rs +++ b/app/gui/src/presenter/graph.rs @@ -19,6 +19,7 @@ use ide_view as view; use ide_view::graph_editor::component::node as node_view; use ide_view::graph_editor::component::visualization as visualization_view; use ide_view::graph_editor::EdgeEndpoint; +use view::graph_editor::ExecutionEnvironment; use view::graph_editor::WidgetUpdates; @@ -82,13 +83,15 @@ pub fn default_node_position() -> Vector2 { #[derive(Debug)] struct Model { - project: model::Project, - controller: controller::ExecutedGraph, - view: view::graph_editor::GraphEditor, - state: Rc, - _visualization: Visualization, - widget: controller::Widget, - _execution_stack: CallStack, + project: model::Project, + controller: controller::ExecutedGraph, + view: view::graph_editor::GraphEditor, + state: Rc, + _visualization: Visualization, + widget: controller::Widget, + _execution_stack: CallStack, + // TODO(#5930): Move me once we synchronise the execution environment with the language server. + execution_environment: Rc>, } impl Model { @@ -115,6 +118,7 @@ impl Model { _visualization: visualization, widget, _execution_stack: execution_stack, + execution_environment: Default::default(), } } @@ -166,16 +170,6 @@ impl Model { ); } - /// TODO(#5930): Provide the state of the output context in the current environment. - fn output_context_enabled(&self) -> bool { - true - } - - /// TODO(#5930): Provide the current execution environment of the project. - fn execution_environment(&self) -> &str { - "design" - } - /// Sets or clears a context switch expression for the specified node. /// /// A context switch expression allows enabling or disabling the execution of a particular node @@ -185,22 +179,21 @@ impl Model { /// /// The behavior of this function can be summarized in the following table: /// ```ignore - /// | Context Enabled | Active | Action | - /// |-----------------|-------------|--------------| - /// | Yes | Yes | Add Disable | - /// | Yes | No | Clear | - /// | No | Yes | Add Enable | - /// | No | No | Clear | + /// | Global Context Permission | Active | Action | + /// |---------------------------|-------------|--------------| + /// | Enabled | Yes | Add Disable | + /// | Enabled | No | Clear | + /// | Disabled | Yes | Add Enable | + /// | Disabled | No | Clear | /// ``` - /// TODO(#5929): Connect this function with buttons on nodes. - #[allow(dead_code)] fn node_action_context_switch(&self, id: ViewNodeId, active: bool) { let context = Context::Output; - let current_state = self.output_context_enabled(); - let environment = self.execution_environment().into(); + let environment = self.execution_environment.get(); + let current_state = environment.output_context_enabled(); let switch = if current_state { ContextSwitch::Disable } else { ContextSwitch::Enable }; let expr = if active { - Some(ContextSwitchExpression { switch, context, environment }) + let environment_name = environment.to_string().into(); + Some(ContextSwitchExpression { switch, context, environment: environment_name }) } else { None }; @@ -500,6 +493,15 @@ impl Model { } } } + + fn toggle_execution_environment(&self) -> ExecutionEnvironment { + let new_environment = match self.execution_environment.get() { + ExecutionEnvironment::Live => ExecutionEnvironment::Design, + ExecutionEnvironment::Design => ExecutionEnvironment::Live, + }; + self.execution_environment.set(new_environment); + new_environment + } } @@ -538,18 +540,14 @@ impl ExpressionUpdate { self.freeze_updated.map(|freeze| (self.id, freeze)) } - /// An updated status of output context switch (`true` if output context is explicitly enabled - /// for the node, `false` otherwise). `None` if the status was not updated. - fn output_context(&self) -> Option<(ViewNodeId, bool)> { + /// An updated status of output context switch: `true` (or `false`) if the output context was + /// explicitly enabled (or disabled) for the node, `None` otherwise. The outer `Option` is + /// `None` if the status was not updated. + fn output_context(&self) -> Option<(ViewNodeId, Option)> { self.context_switch_updated.as_ref().map(|context_switch_expr| { - use Context::*; - use ContextSwitch::*; - let enabled = match context_switch_expr { - Some(ContextSwitchExpression { switch: Enable, context: Output, .. }) => true, - Some(ContextSwitchExpression { switch: Disable, context: Output, .. }) => false, - None => false, - }; - (self.id, enabled) + let switch = + context_switch_expr.as_ref().map(|expr| expr.switch == ContextSwitch::Enable); + (self.id, switch) }) } } @@ -720,6 +718,14 @@ impl Graph { })); + // === Execution Environment === + + // TODO(#5930): Delete me once we synchronise the execution environment with the + // language server. + view.set_execution_environment <+ view.toggle_execution_environment.map( + f_!(model.toggle_execution_environment())); + + // === Refreshing Nodes === remove_node <= update_data.map(|update| update.remove_nodes()); @@ -727,12 +733,7 @@ impl Graph { update_node_expression <- expression_update.map(ExpressionUpdate::expression); set_node_skip <- expression_update.filter_map(ExpressionUpdate::skip); set_node_freeze <- expression_update.filter_map(ExpressionUpdate::freeze); - // TODO(#5930): Use project model to retrieve a current state of the output context. - output_context_enabled <- update_view.constant(true); - output_context_updated <- expression_update.filter_map(ExpressionUpdate::output_context); - _context_switch_highlighted <- output_context_updated.map2(&output_context_enabled, - |(node_id, enabled_for_node), enabled_globally| (*node_id, enabled_for_node != enabled_globally) - ); + set_node_context_switch <- expression_update.filter_map(ExpressionUpdate::output_context); set_node_position <= update_data.map(|update| update.set_node_positions()); set_node_visualization <= update_data.map(|update| update.set_node_visualizations()); enable_vis <- set_node_visualization.filter_map(|(id,path)| path.is_some().as_some(*id)); @@ -741,8 +742,7 @@ impl Graph { view.set_node_expression <+ update_node_expression; view.set_node_skip <+ set_node_skip; view.set_node_freeze <+ set_node_freeze; - // TODO (#5929): Connect to the view when the API is ready. - // view.highlight_output_context_switch <+ context_switch_highlighted; + view.set_node_context_switch <+ set_node_context_switch; view.set_node_position <+ set_node_position; view.set_visualization <+ set_node_visualization; view.enable_visualization <+ enable_vis; @@ -789,6 +789,7 @@ impl Graph { eval view.nodes_collapsed(((nodes, _)) model.nodes_collapsed(nodes)); eval view.enabled_visualization_path(((node_id, path)) model.node_visualization_changed(*node_id, path.clone())); eval view.node_expression_span_set(((node_id, crumbs, expression)) model.node_expression_span_set(*node_id, crumbs, expression.clone_ref())); + eval view.node_action_context_switch(((node_id, active)) model.node_action_context_switch(*node_id, *active)); eval view.node_action_skip(((node_id, enabled)) model.node_action_skip(*node_id, *enabled)); eval view.node_action_freeze(((node_id, enabled)) model.node_action_freeze(*node_id, *enabled)); eval view.request_import((import_path) model.add_import_if_missing(import_path)); diff --git a/app/gui/view/examples/icons/src/lib.rs b/app/gui/view/examples/icons/src/lib.rs index 12bda547dba0..b3620c9cc6d6 100644 --- a/app/gui/view/examples/icons/src/lib.rs +++ b/app/gui/view/examples/icons/src/lib.rs @@ -94,13 +94,13 @@ pub fn entry_point_icons() { skip_icon.color_rgba.set(dark_green.into()); place_icon(&world, skip_icon, 20.0, y); - let disable_reevaluation_icon = action_bar::icon::disable_reevaluation::View::new(); - disable_reevaluation_icon.color_rgba.set(dark_green.into()); - place_icon(&world, disable_reevaluation_icon, 40.0, y); + let disable_output_context_icon = action_bar::icon::disable_output_context::View::new(); + disable_output_context_icon.color_rgba.set(dark_green.into()); + place_icon(&world, disable_output_context_icon, 40.0, y); - let enable_reevaluation_icon = action_bar::icon::enable_reevaluation::View::new(); - enable_reevaluation_icon.color_rgba.set(dark_green.into()); - place_icon(&world, enable_reevaluation_icon, 60.0, y); + let enable_output_context_icon = action_bar::icon::enable_output_context::View::new(); + enable_output_context_icon.color_rgba.set(dark_green.into()); + place_icon(&world, enable_output_context_icon, 60.0, y); } /// Create a grid with pixel squares to help development of icons. diff --git a/app/gui/view/graph-editor/src/component/node.rs b/app/gui/view/graph-editor/src/component/node.rs index 5a866d8e2ea8..b8b7daa3850c 100644 --- a/app/gui/view/graph-editor/src/component/node.rs +++ b/app/gui/view/graph-editor/src/component/node.rs @@ -11,6 +11,7 @@ use crate::component::visualization; use crate::selection::BoundingBox; use crate::tooltip; use crate::view; +use crate::ExecutionEnvironment; use crate::Type; use crate::WidgetUpdates; @@ -306,6 +307,9 @@ ensogl::define_endpoints_2! { edit_expression (text::Range, ImString), set_skip_macro (bool), set_freeze_macro (bool), + /// Set whether the output context is explicitly enabled: `Some(true/false)` for + /// enabled/disabled; `None` for no context switch expression. + set_context_switch (Option), set_comment (Comment), set_error (Option), /// Set the expression USAGE type. This is not the definition type, which can be set with @@ -326,7 +330,8 @@ ensogl::define_endpoints_2! { set_profiling_max_global_duration (f32), set_profiling_status (profiling::Status), /// Indicate whether on hover the quick action icons should appear. - show_quick_action_bar_on_hover (bool) + show_quick_action_bar_on_hover (bool), + set_execution_environment (ExecutionEnvironment), } Output { /// Press event. Emitted when user clicks on non-active part of the node, like its @@ -342,6 +347,7 @@ ensogl::define_endpoints_2! { /// and update the node with new expression tree using `set_expression`. on_expression_modified (span_tree::Crumbs, ImString), comment (Comment), + context_switch (bool), skip (bool), freeze (bool), hover (bool), @@ -801,6 +807,7 @@ impl Node { // === Action Bar === let visualization_button_state = action_bar.action_visibility.clone_ref(); + out.context_switch <+ action_bar.action_context_switch; out.skip <+ action_bar.action_skip; out.freeze <+ action_bar.action_freeze; show_action_bar <- out.hover && input.show_quick_action_bar_on_hover; @@ -808,6 +815,8 @@ impl Node { eval input.show_quick_action_bar_on_hover((value) action_bar.show_on_hover(value)); action_bar.set_action_freeze_state <+ input.set_freeze_macro; action_bar.set_action_skip_state <+ input.set_skip_macro; + action_bar.set_action_context_switch_state <+ input.set_context_switch; + action_bar.set_execution_environment <+ input.set_execution_environment; // === View Mode === diff --git a/app/gui/view/graph-editor/src/component/node/action_bar.rs b/app/gui/view/graph-editor/src/component/node/action_bar.rs index 876c48349c5a..e385baa512b4 100644 --- a/app/gui/view/graph-editor/src/component/node/action_bar.rs +++ b/app/gui/view/graph-editor/src/component/node/action_bar.rs @@ -3,6 +3,8 @@ use crate::prelude::*; use ensogl::display::shape::*; +use crate::ExecutionEnvironment; + use enso_config::ARGS; use enso_frp as frp; use ensogl::application::tooltip; @@ -31,9 +33,11 @@ const BUTTON_OFFSET: f32 = 0.5; /// Grow the hover area in x direction by this amount. Used to close the gap between action /// icons and node. const HOVER_EXTENSION_X: f32 = 15.0; +const VISIBILITY_TOOLTIP_LABEL: &str = "Show preview"; +const DISABLE_OUTPUT_CONTEXT_TOOLTIP_LABEL: &str = "Don't write to files and databases"; +const ENABLE_OUTPUT_CONTEXT_TOOLTIP_LABEL: &str = "Allow writing to files and databases"; const FREEZE_TOOLTIP_LABEL: &str = "Freeze"; const SKIP_TOOLTIP_LABEL: &str = "Skip"; -const VISIBILITY_TOOLTIP_LABEL: &str = "Show preview"; // =============== @@ -65,20 +69,25 @@ mod hover_area { ensogl::define_endpoints! { Input { - set_size (Vector2), - set_visibility (bool), - set_action_visibility_state (bool), - set_action_skip_state (bool), - set_action_freeze_state (bool), - show_on_hover (bool), + set_size (Vector2), + set_visibility (bool), + set_action_visibility_state (bool), + set_action_skip_state (bool), + set_action_freeze_state (bool), + /// Set whether the output context is explicitly enabled: `Some(true/false)` for + /// enabled/disabled; `None` for no context switch expression. + set_action_context_switch_state (Option), + show_on_hover (bool), + set_execution_environment (ExecutionEnvironment), } Output { - mouse_over (), - mouse_out (), - action_visibility (bool), - action_freeze (bool), - action_skip (bool), + mouse_over (), + mouse_out (), + action_visibility (bool), + action_context_switch (bool), + action_freeze (bool), + action_skip (bool), } } @@ -91,29 +100,40 @@ ensogl::define_endpoints! { #[derive(Clone, CloneRef, Debug)] struct Icons { display_object: display::object::Instance, - freeze: ToggleButton, visibility: ToggleButton, + context_switch: ContextSwitchButton, + freeze: ToggleButton, skip: ToggleButton, } impl Icons { fn new(app: &Application) -> Self { let display_object = display::object::Instance::new(); - let freeze = labeled_button(app, FREEZE_TOOLTIP_LABEL); let visibility = labeled_button(app, VISIBILITY_TOOLTIP_LABEL); + let context_switch = ContextSwitchButton::enable(app); + let freeze = labeled_button(app, FREEZE_TOOLTIP_LABEL); let skip = labeled_button(app, SKIP_TOOLTIP_LABEL); display_object.add_child(&visibility); + display_object.add_child(&context_switch); if ARGS.groups.feature_preview.options.skip_and_freeze.value { display_object.add_child(&freeze); display_object.add_child(&skip); } - Self { display_object, freeze, visibility, skip } + Self { display_object, visibility, context_switch, freeze, skip } } fn set_visibility(&self, visible: bool) { + self.visibility.frp.set_visibility(visible); + self.context_switch.set_visibility(visible); self.freeze.frp.set_visibility(visible); self.skip.frp.set_visibility(visible); - self.visibility.frp.set_visibility(visible); + } + + fn set_color_scheme(&self, color_scheme: &toggle_button::ColorScheme) { + self.visibility.frp.set_color_scheme(color_scheme); + self.context_switch.set_color_scheme(color_scheme); + self.freeze.frp.set_color_scheme(color_scheme); + self.skip.frp.set_color_scheme(color_scheme); } } @@ -130,6 +150,77 @@ fn labeled_button(app: &Application, label: &str) -> Toggl +// ============================= +// === Context Switch Button === +// ============================= + +/// A button to enable/disable the output context for a particular node. It holds two buttons +/// internally for each shape, but only one is shown at a time, based on the execution environment +/// which sets the global permission for the output context. +#[derive(Clone, CloneRef, Debug)] +struct ContextSwitchButton { + globally_enabled: Rc>, + disable_button: ToggleButton, + enable_button: ToggleButton, + display_object: display::object::Instance, +} + +impl ContextSwitchButton { + fn enable(app: &Application) -> Self { + let display_object = display::object::Instance::new(); + let disable_button = labeled_button(app, DISABLE_OUTPUT_CONTEXT_TOOLTIP_LABEL); + let enable_button = labeled_button(app, ENABLE_OUTPUT_CONTEXT_TOOLTIP_LABEL); + disable_button.set_size((100.pc(), 100.pc())); + enable_button.set_size((100.pc(), 100.pc())); + display_object.add_child(&enable_button); + let globally_enabled = Rc::new(Cell::new(false)); + Self { globally_enabled, disable_button, enable_button, display_object } + } + + /// Set the button's on/off state based on whether the output context is explicitly enabled for + /// this node. `output_context_enabled` is `Some(true/false)` for enabled/disabled; `None` for + /// no context switch expression. + fn set_state(&self, output_context_enabled: Option) { + let disable_button_active = !output_context_enabled.unwrap_or(true); + self.disable_button.set_state(disable_button_active); + let enable_button_active = output_context_enabled.unwrap_or(false); + self.enable_button.set_state(enable_button_active); + } + + /// Swap the buttons if the execution environment changed. + fn set_execution_environment(&self, environment: &ExecutionEnvironment) { + if environment.output_context_enabled() != self.globally_enabled.get() { + if environment.output_context_enabled() { + self.remove_child(&self.enable_button); + self.add_child(&self.disable_button); + self.globally_enabled.set(true); + } else { + self.remove_child(&self.disable_button); + self.add_child(&self.enable_button); + self.globally_enabled.set(false); + } + } + } + + fn set_visibility(&self, visible: bool) { + self.disable_button.set_visibility(visible); + self.enable_button.set_visibility(visible); + } + + fn set_color_scheme(&self, color_scheme: &toggle_button::ColorScheme) { + self.disable_button.set_color_scheme(color_scheme); + self.enable_button.set_color_scheme(color_scheme); + } +} + +impl display::Object for ContextSwitchButton { + fn display_object(&self) -> &display::object::Instance { + &self.display_object + } +} + + + // ======================== // === Action Bar Model === // ======================== @@ -155,14 +246,18 @@ impl Model { let styles = StyleWatch::new(&scene.style_sheet); shapes.add_sub_shape(&hover_area); - shapes.add_sub_shape(&icons.freeze.view()); shapes.add_sub_shape(&icons.visibility.view()); + shapes.add_sub_shape(&icons.context_switch.disable_button.view()); + shapes.add_sub_shape(&icons.context_switch.enable_button.view()); + shapes.add_sub_shape(&icons.freeze.view()); shapes.add_sub_shape(&icons.skip.view()); ensogl::shapes_order_dependencies! { scene => { - hover_area -> icon::freeze; hover_area -> icon::visibility; + hover_area -> icon::disable_output_context; + hover_area -> icon::enable_output_context; + hover_area -> icon::freeze; hover_area -> icon::skip; } } @@ -176,13 +271,13 @@ impl Model { self } - fn place_button_in_slot(&self, button: &ToggleButton, index: usize) { + fn place_button_in_slot(&self, button: &dyn display::Object, index: usize) { let icon_size = self.icon_size(); let index = index as f32; let padding = BUTTON_PADDING; let offset = BUTTON_OFFSET; button.set_x(((1.0 + padding) * index + offset) * icon_size.x); - button.frp.set_size(icon_size); + button.set_size(icon_size); } fn icon_size(&self) -> Vector2 { @@ -211,17 +306,18 @@ impl Model { self.icons.set_x(-size.x / 2.0); self.place_button_in_slot(&self.icons.visibility, 0); + self.place_button_in_slot(&self.icons.context_switch, 1); if ARGS.groups.feature_preview.options.skip_and_freeze.value { - self.place_button_in_slot(&self.icons.skip, 1); - self.place_button_in_slot(&self.icons.freeze, 2); + self.place_button_in_slot(&self.icons.skip, 2); + self.place_button_in_slot(&self.icons.freeze, 3); } let buttons_count = if ARGS.groups.feature_preview.options.skip_and_freeze.value { // Toggle visualization, skip and freeze buttons. - 3 + 4 } else { // Toggle visualization button only. - 1 + 2 }; self.layout_hover_area_to_cover_buttons(buttons_count); @@ -287,6 +383,12 @@ impl ActionBar { eval frp.set_action_visibility_state ((state) model.icons.visibility.set_state(state)); eval frp.set_action_skip_state ((state) model.icons.skip.set_state(state)); eval frp.set_action_freeze_state ((state) model.icons.freeze.set_state(state)); + eval frp.set_action_context_switch_state ((state) + model.icons.context_switch.set_state(*state) + ); + eval frp.set_execution_environment ((environment) + model.icons.context_switch.set_execution_environment(environment) + ); // === Mouse Interactions === @@ -300,9 +402,45 @@ impl ActionBar { // === Icon Actions === - frp.source.action_skip <+ model.icons.skip.state; - frp.source.action_freeze <+ model.icons.freeze.state; frp.source.action_visibility <+ model.icons.visibility.state; + frp.source.action_skip <+ model.icons.skip.state; + frp.source.action_freeze <+ model.icons.freeze.state; + disable_context_button_clicked <- model.icons.context_switch.disable_button.is_pressed.on_true(); + enable_context_button_clicked <- model.icons.context_switch.enable_button.is_pressed.on_true(); + output_context_disabled <- model.icons.context_switch.disable_button.state + .sample(&disable_context_button_clicked); + output_context_enabled <- model.icons.context_switch.enable_button.state + .sample(&enable_context_button_clicked); + frp.source.action_context_switch <+ any(&output_context_disabled, &output_context_enabled); + // Setting the state of the context switch button is necessary because e.g. toggling + // the "enable" button when there's a "disable" expression should cause the "disable" + // button to change state as well. + frp.set_action_context_switch_state <+ output_context_disabled.map2( + &model.icons.context_switch.enable_button.state, + |disabled, enabled| { + match (disabled, enabled) { + (true, _) => Some(false), + (false, false) => None, + (false, true) => { + error!("Invalid node action bar button state: context switch buttons were both on."); + Some(true) + } + } + } + ); + frp.set_action_context_switch_state <+ output_context_enabled.map2( + &model.icons.context_switch.disable_button.state, + |enabled, disabled| { + match (enabled, disabled) { + (true, _) => Some(true), + (false, false) => None, + (false, true) => { + error!("Invalid node action bar button state: context switch buttons were both on."); + Some(false) + } + } + } + ); } let color_scheme = toggle_button::ColorScheme { @@ -320,10 +458,7 @@ impl ActionBar { ), ..default() }; - - model.icons.freeze.frp.set_color_scheme(&color_scheme); - model.icons.skip.frp.set_color_scheme(&color_scheme); - model.icons.visibility.frp.set_color_scheme(&color_scheme); + model.icons.set_color_scheme(&color_scheme); frp.show_on_hover.emit(true); visibility_init.emit(false); diff --git a/app/gui/view/graph-editor/src/component/node/action_bar/icon.rs b/app/gui/view/graph-editor/src/component/node/action_bar/icon.rs index 3f9686c77532..df1dc90bc259 100644 --- a/app/gui/view/graph-editor/src/component/node/action_bar/icon.rs +++ b/app/gui/view/graph-editor/src/component/node/action_bar/icon.rs @@ -167,8 +167,8 @@ pub mod skip { } } -/// Icon for the button to disable re-evaluation. Looks like a crossed-out arrow loop. -pub mod disable_reevaluation { +/// Icon for the button to disable the output context. Looks like a crossed-out arrow loop. +pub mod disable_output_context { use super::*; ensogl::shape! { @@ -198,8 +198,8 @@ pub mod disable_reevaluation { } } -/// Icon for the button to enable re-evaluation. Looks like an arrow loop. -pub mod enable_reevaluation { +/// Icon for the button to enable the output context. Looks like an arrow loop. +pub mod enable_output_context { use super::*; ensogl::shape! { diff --git a/app/gui/view/graph-editor/src/component/visualization/container.rs b/app/gui/view/graph-editor/src/component/visualization/container.rs index 183fd6c7c10f..5d713495cca5 100644 --- a/app/gui/view/graph-editor/src/component/visualization/container.rs +++ b/app/gui/view/graph-editor/src/component/visualization/container.rs @@ -526,6 +526,19 @@ impl Container { } + // === Visualisation Chooser Bindings === + + frp::extend! { network + selected_definition <- action_bar.visualisation_selection.map(f!([registry](path) + path.as_ref().and_then(|path| registry.definition_from_path(path)) + )); + action_bar.hide_icons <+ selected_definition.constant(()); + frp.source.vis_input_type <+ frp.set_vis_input_type; + let chooser = &model.action_bar.visualization_chooser(); + chooser.frp.set_vis_input_type <+ frp.set_vis_input_type; + } + + // === Cycling Visualizations === frp::extend! { network @@ -538,7 +551,12 @@ impl Container { // === Switching Visualizations === frp::extend! { network - new_vis_definition <- any(frp.set_visualization,vis_after_cycling,default_visualisation); + vis_definition_set <- any( + frp.set_visualization, + selected_definition, + vis_after_cycling, + default_visualisation); + new_vis_definition <- vis_definition_set.on_change(); let preprocessor = &frp.source.preprocessor; frp.source.visualisation <+ new_vis_definition.map(f!( [model,action_bar,app,preprocessor](vis_definition) { @@ -625,32 +643,6 @@ impl Container { } - // === Visualisation chooser frp bindings === - - frp::extend! { network - selected_definition <- action_bar.visualisation_selection.map(f!([registry](path) - path.as_ref().and_then(|path| registry.definition_from_path(path)) - )); - eval selected_definition([app,model,preprocessor](definition) { - let vis = definition.as_ref().map(|d| d.new_instance(&app)); - match vis { - Some(Ok(vis)) => model.set_visualization(vis,&preprocessor), - Some(Err(err)) => { - warn!("Failed to instantiate visualisation: {err:?}"); - }, - None => warn!("Invalid visualisation selected."), - }; - }); - frp.source.visualisation <+ selected_definition; - on_selected <- selected_definition.map(|d|d.as_ref().map(|_|())).unwrap(); - eval_ on_selected ( action_bar.hide_icons.emit(()) ); - frp.source.vis_input_type <+ frp.set_vis_input_type; - eval frp.set_vis_input_type ( - (tp) model.action_bar.visualization_chooser().frp.set_vis_input_type(tp) - ); - } - - // === Action bar actions === frp::extend! { network diff --git a/app/gui/view/graph-editor/src/component/visualization/container/action_bar.rs b/app/gui/view/graph-editor/src/component/visualization/container/action_bar.rs index 2fdeb1dedb58..f9e2cd28ea5e 100644 --- a/app/gui/view/graph-editor/src/component/visualization/container/action_bar.rs +++ b/app/gui/view/graph-editor/src/component/visualization/container/action_bar.rs @@ -370,9 +370,7 @@ impl ActionBar { eval_ frp.hide_icons ( model.hide() ); eval_ frp.show_icons ( model.show() ); - eval frp.input.set_selected_visualization ((vis){ - visualization_chooser.input.set_selected.emit(vis); - }); + visualization_chooser.input.set_selected <+ frp.input.set_selected_visualization; // === Mouse Interactions === diff --git a/app/gui/view/graph-editor/src/component/visualization/container/visualization_chooser.rs b/app/gui/view/graph-editor/src/component/visualization/container/visualization_chooser.rs index 778214a99a39..a9134383a4c8 100644 --- a/app/gui/view/graph-editor/src/component/visualization/container/visualization_chooser.rs +++ b/app/gui/view/graph-editor/src/component/visualization/container/visualization_chooser.rs @@ -117,7 +117,7 @@ impl VisualizationChooser { eval_ frp.hide_selection_menu ( menu.hide_selection_menu.emit(()) ); eval frp.set_menu_offset_y ((offset) menu.set_menu_offset_y.emit(offset) ); - set_selected_ix <= frp.input.set_selected.map2(&frp.output.entries,|selected,entries| + set_selected_ix <= all_with(&frp.input.set_selected, &frp.output.entries, |selected,entries| selected.as_ref().map(|s| entries.iter().position(|item| item == s)) ); eval set_selected_ix ((ix) menu.set_selected.emit(ix)); diff --git a/app/gui/view/graph-editor/src/lib.rs b/app/gui/view/graph-editor/src/lib.rs index 3bdac672052e..2fbd9f829720 100644 --- a/app/gui/view/graph-editor/src/lib.rs +++ b/app/gui/view/graph-editor/src/lib.rs @@ -578,6 +578,13 @@ ensogl::define_endpoints_2! { toggle_profiling_mode(), + // === Execution Environment === + + set_execution_environment(ExecutionEnvironment), + // TODO(#5930): Temporary shortcut for testing different execution environments + toggle_execution_environment(), + + // === Debug === /// Enable or disable debug-only features. @@ -618,6 +625,9 @@ ensogl::define_endpoints_2! { edit_node_expression ((NodeId, text::Range, ImString)), set_node_skip ((NodeId,bool)), set_node_freeze ((NodeId,bool)), + /// Set whether the output context is explicitly enabled for a node: `Some(true/false)` for + /// enabled/disabled; `None` for no context switch expression. + set_node_context_switch ((NodeId, Option)), set_node_comment ((NodeId,node::Comment)), set_node_position ((NodeId,Vector2)), set_expression_usage_type ((NodeId,ast::Id,Option)), @@ -695,26 +705,27 @@ ensogl::define_endpoints_2! { // === Other === // FIXME: To be refactored - node_added (NodeId, Option, bool), - node_removed (NodeId), - nodes_collapsed ((Vec, NodeId)), - node_hovered (Option>), - node_selected (NodeId), - node_deselected (NodeId), - node_position_set ((NodeId,Vector2)), - node_position_set_batched ((NodeId,Vector2)), - node_expression_set ((NodeId,ImString)), - node_expression_span_set ((NodeId, span_tree::Crumbs, ImString)), - node_expression_edited ((NodeId,ImString,Vec>)), - node_comment_set ((NodeId,String)), - node_entered (NodeId), - node_exited (), - node_editing_started (NodeId), - node_editing_finished (NodeId), - node_action_freeze ((NodeId, bool)), - node_action_skip ((NodeId, bool)), - node_edit_mode (bool), - nodes_labels_visible (bool), + node_added (NodeId, Option, bool), + node_removed (NodeId), + nodes_collapsed ((Vec, NodeId)), + node_hovered (Option>), + node_selected (NodeId), + node_deselected (NodeId), + node_position_set ((NodeId,Vector2)), + node_position_set_batched ((NodeId,Vector2)), + node_expression_set ((NodeId,ImString)), + node_expression_span_set ((NodeId, span_tree::Crumbs, ImString)), + node_expression_edited ((NodeId,ImString,Vec>)), + node_comment_set ((NodeId,String)), + node_entered (NodeId), + node_exited (), + node_editing_started (NodeId), + node_editing_finished (NodeId), + node_action_context_switch ((NodeId, bool)), + node_action_freeze ((NodeId, bool)), + node_action_skip ((NodeId, bool)), + node_edit_mode (bool), + nodes_labels_visible (bool), /// `None` value as a visualization path denotes a disabled visualization. @@ -1631,6 +1642,10 @@ impl GraphEditorModelWithNetwork { // === Actions === + model.frp.private.output.node_action_context_switch <+ node.view.context_switch.map( + f!([] (active) (node_id, *active)) + ); + eval node.view.freeze ((is_frozen) { model.frp.private.output.node_action_freeze.emit((node_id,*is_frozen)); }); @@ -1695,6 +1710,11 @@ impl GraphEditorModelWithNetwork { let profiling_max_duration = &self.model.profiling_statuses.max_duration; node.set_profiling_max_global_duration <+ self.model.profiling_statuses.max_duration; node.set_profiling_max_global_duration(profiling_max_duration.value()); + + + // === Execution Environment === + + node.set_execution_environment <+ self.model.frp.set_execution_environment; } @@ -2013,6 +2033,13 @@ impl GraphEditorModel { } } + fn set_node_context_switch(&self, node_id: impl Into, context_switch: &Option) { + let node_id = node_id.into(); + if let Some(node) = self.nodes.get_cloned_ref(&node_id) { + node.set_context_switch(*context_switch); + } + } + fn set_node_comment(&self, node_id: impl Into, comment: impl Into) { let node_id = node_id.into(); let comment = comment.into(); @@ -2685,6 +2712,8 @@ impl application::View for GraphEditor { (Press, "debug_mode", "ctrl shift enter", "debug_push_breadcrumb"), (Press, "debug_mode", "ctrl shift up", "debug_pop_breadcrumb"), (Press, "debug_mode", "ctrl n", "add_node_at_cursor"), + // TODO(#5930): Temporary shortcut for testing different execution environments + (Press, "", "cmd shift c", "toggle_execution_environment"), ] .iter() .map(|(a, b, c, d)| Self::self_shortcut_when(*a, *c, *d, *b)) @@ -3183,11 +3212,14 @@ fn new_graph_editor(app: &Application) -> GraphEditor { } - // === Set Node SKIP and FREEZE macros === + // === Set Node SKIP/FREEZE macros and context switch expression === frp::extend! { network eval inputs.set_node_skip(((id, skip)) model.set_node_skip(id, skip)); eval inputs.set_node_freeze(((id, freeze)) model.set_node_freeze(id, freeze)); + eval inputs.set_node_context_switch(((id, context_switch)) + model.set_node_context_switch(id, context_switch) + ); } @@ -3904,6 +3936,47 @@ impl display::Object for GraphEditor { +// ============================= +// === Execution Environment === +// ============================= + +// TODO(#5930): Move me once we synchronise the execution environment with the language server. +/// The execution environment which controls the global execution of functions with side effects. +/// +/// For more information, see +/// https://github.com/enso-org/design/blob/main/epics/basic-libraries/write-action-control/design.md. +#[derive(Debug, Clone, CloneRef, Copy, Default)] +pub enum ExecutionEnvironment { + /// Allows editing the graph, but the `Output` context is disabled, so it prevents accidental + /// changes. + #[default] + Design, + /// Unrestricted, live editing of data. + Live, +} + +impl ExecutionEnvironment { + /// Returns whether the output context is enabled for this execution environment. + pub fn output_context_enabled(&self) -> bool { + match self { + Self::Design => false, + Self::Live => true, + } + } +} + +impl Display for ExecutionEnvironment { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let name = match self { + Self::Design => "design", + Self::Live => "live", + }; + write!(f, "{name}") + } +} + + + // ============= // === Tests === // ============= diff --git a/app/ide-desktop/eslint.config.js b/app/ide-desktop/eslint.config.js index 4260ce50e633..cb7120a7b86d 100644 --- a/app/ide-desktop/eslint.config.js +++ b/app/ide-desktop/eslint.config.js @@ -113,7 +113,7 @@ const RESTRICTED_SYNTAXES = [ }, { // Matches functions and arrow functions, but not methods. - selector: `:matches(FunctionDeclaration[id.name=${NOT_PASCAL_CASE}]:has(${JSX}), VariableDeclarator[id.name=${NOT_PASCAL_CASE}]:has(:matches(ArrowFunctionExpression ${JSX}, ${WITH_ROUTER})))`, + selector: `:matches(FunctionDeclaration[id.name=${NOT_PASCAL_CASE}]:has(${JSX}), VariableDeclarator[id.name=${NOT_PASCAL_CASE}]:has(:matches(ArrowFunctionExpression.init ${JSX}, ${WITH_ROUTER})))`, message: 'Use `PascalCase` for React components', }, { @@ -218,9 +218,6 @@ export default [ ...globals.browser, ...globals.node, ...globals.es2015, - BUNDLED_ENGINE_VERSION: true, - PROJECT_MANAGER_IN_BUNDLE_PATH: true, - BUILD_INFO: true, }, }, rules: { diff --git a/app/ide-desktop/lib/client/src/authentication.ts b/app/ide-desktop/lib/client/src/authentication.ts index ceb9c5a5334f..5b866dcc5249 100644 --- a/app/ide-desktop/lib/client/src/authentication.ts +++ b/app/ide-desktop/lib/client/src/authentication.ts @@ -74,6 +74,9 @@ import * as electron from 'electron' import opener from 'opener' +import * as fs from 'node:fs' +import * as os from 'node:os' +import * as path from 'node:path' import * as common from 'enso-common' import * as contentConfig from 'enso-content-config' @@ -104,6 +107,7 @@ const OPEN_URL_EVENT = 'open-url' export function initModule(window: () => electron.BrowserWindow) { initIpc() initOpenUrlListener(window) + initSaveAccessTokenListener() } /** Registers an Inter-Process Communication (IPC) channel between the Electron application and the @@ -139,3 +143,37 @@ function initOpenUrlListener(window: () => electron.BrowserWindow) { } }) } + +/** Registers a listener that fires a callback for `save-access-token` events. + * + * This listener is used to save given access token to credentials file to be later used by enso backend. + * + * Credentials file is placed in users home directory in `.enso` subdirectory in `credentials` file. */ +function initSaveAccessTokenListener() { + electron.ipcMain.on(ipc.Channel.saveAccessToken, (event, accessToken: string) => { + /** Enso home directory for credentials file. */ + const ensoCredentialsDirectoryName = '.enso' + /** Enso credentials file. */ + const ensoCredentialsFileName = 'credentials' + /** System agnostic credentials directory home path. */ + const ensoCredentialsHomePath = path.join(os.homedir(), ensoCredentialsDirectoryName) + + fs.mkdir(ensoCredentialsHomePath, { recursive: true }, err => { + if (err) { + logger.error(`Couldn't create ${ensoCredentialsDirectoryName} directory.`) + } else { + fs.writeFile( + path.join(ensoCredentialsHomePath, ensoCredentialsFileName), + accessToken, + err => { + if (err) { + logger.error(`Could not write to ${ensoCredentialsFileName} file.`) + } + } + ) + } + }) + + event.preventDefault() + }) +} diff --git a/app/ide-desktop/lib/client/src/ipc.ts b/app/ide-desktop/lib/client/src/ipc.ts index 8299aaf08613..301ab8615cf7 100644 --- a/app/ide-desktop/lib/client/src/ipc.ts +++ b/app/ide-desktop/lib/client/src/ipc.ts @@ -19,4 +19,6 @@ export enum Channel { setDeepLinkHandler = 'set-deep-link-handler', /** Channel for signaling that a deep link to this application was opened. */ openDeepLink = 'open-deep-link', + /** Channel for signaling that access token be saved to a credentials file. */ + saveAccessToken = 'save-access-token', } diff --git a/app/ide-desktop/lib/client/src/preload.ts b/app/ide-desktop/lib/client/src/preload.ts index e82418039957..311f5a89c22c 100644 --- a/app/ide-desktop/lib/client/src/preload.ts +++ b/app/ide-desktop/lib/client/src/preload.ts @@ -105,5 +105,12 @@ const AUTHENTICATION_API = { electron.ipcRenderer.on(ipc.Channel.openDeepLink, (_event, url: string) => { callback(url) }), + /** Saves the access token to a credentials file. + * + * Enso backend doesn't have access to Electron localStorage so we need to save access token to a file. + * Then the token will be used to sign cloud API requests. */ + saveAccessToken: (accessToken: string) => { + electron.ipcRenderer.send(ipc.Channel.saveAccessToken, accessToken) + }, } electron.contextBridge.exposeInMainWorld(AUTHENTICATION_API_KEY, AUTHENTICATION_API) diff --git a/app/ide-desktop/lib/client/watch.ts b/app/ide-desktop/lib/client/watch.ts index fdc61ed181a3..81b89bd5d66d 100644 --- a/app/ide-desktop/lib/client/watch.ts +++ b/app/ide-desktop/lib/client/watch.ts @@ -110,7 +110,7 @@ await fs.symlink( 'dir' ) -const ELECTRON_ARGS = [path.join(IDE_DIR_PATH, 'index.cjs'), ...process.argv.slice(2)] +const ELECTRON_ARGS = [path.join(IDE_DIR_PATH, 'index.cjs'), '--', ...process.argv.slice(2)] process.on('SIGINT', () => { console.log('SIGINT received. Exiting.') diff --git a/app/ide-desktop/lib/content-config/src/config.json b/app/ide-desktop/lib/content-config/src/config.json index 2caaa3da92a7..f1f2b90e4e0a 100644 --- a/app/ide-desktop/lib/content-config/src/config.json +++ b/app/ide-desktop/lib/content-config/src/config.json @@ -1,10 +1,5 @@ { "options": { - "authentication": { - "value": false, - "description": "Determines whether user authentication is enabled. This option is disregarded if the application is executed in the cloud." - }, - "dataCollection": { "value": true, "description": "Determines whether anonymous usage data is to be collected.", @@ -148,6 +143,19 @@ "primary": false } } + }, + "cloud": { + "description": "Options related to cloud authentication, project storage, and project execution.", + "options": { + "authentication": { + "value": false, + "description": "Determines whether user authentication is enabled. This option is disregarded if the application is executed in the cloud." + }, + "dashboard": { + "value": false, + "description": "Determines whether the dashboard (containing the directory listing) is enabled. This option is disregarded if the application is executed in the cloud." + } + } } } } diff --git a/app/ide-desktop/lib/content/esbuild-config.ts b/app/ide-desktop/lib/content/esbuild-config.ts index e436ef30e215..f7a620ebfd68 100644 --- a/app/ide-desktop/lib/content/esbuild-config.ts +++ b/app/ide-desktop/lib/content/esbuild-config.ts @@ -117,7 +117,7 @@ export function bundlerOptions(args: Arguments) { // in `ensogl/pack/js/src/runner/index.ts` name: 'pkg-js-is-cjs', setup: build => { - build.onLoad({ filter: /\/pkg.js$/ }, async ({ path }) => ({ + build.onLoad({ filter: /[/\\]pkg.js$/ }, async ({ path }) => ({ contents: await fs.readFile(path), loader: 'copy', })) diff --git a/app/ide-desktop/lib/content/src/index.html b/app/ide-desktop/lib/content/src/index.html index f505782b9d22..65625b65e350 100644 --- a/app/ide-desktop/lib/content/src/index.html +++ b/app/ide-desktop/lib/content/src/index.html @@ -19,7 +19,7 @@ frame-src 'self' data: https://accounts.google.com https://enso-org.firebaseapp.com; script-src 'self' 'unsafe-eval' data: https://*; style-src 'self' 'unsafe-inline' data: https://*; - connect-src 'self' data: ws://localhost:* ws://127.0.0.1:* http://localhost:* https://*; + connect-src 'self' data: ws://localhost:* ws://127.0.0.1:* http://localhost:* https://* wss://*; worker-src 'self' blob:; img-src 'self' blob: data: https://*; font-src 'self' data: https://*" @@ -40,7 +40,7 @@ -
+