From 76bb17808b7936f24a4d0a32bf9c91cc552bbd38 Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Thu, 10 Aug 2023 14:48:30 +0400 Subject: [PATCH 1/7] Rewrite unit test for new behavior --- Cargo.lock | 23 ++++++++++++++++++++++ app/gui/language/span-tree/Cargo.toml | 1 + app/gui/language/span-tree/src/builder.rs | 6 ++++++ app/gui/language/span-tree/src/generate.rs | 14 ++++++++++++- 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 698c7931ed65..e67c2f4365fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1717,6 +1717,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2ea6706d74fca54e15f1d40b5cf7fe7f764aaec61352a9fcec58fe27e042fc8" +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "difference" version = "2.0.0" @@ -5600,6 +5606,16 @@ dependencies = [ "termtree", ] +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "pretty_env_logger" version = "0.4.0" @@ -6497,6 +6513,7 @@ dependencies = [ "enso-text", "failure", "parser", + "pretty_assertions", "wasm-bindgen-test", ] @@ -7774,6 +7791,12 @@ version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "114ba2b24d2167ef6d67d7d04c8cc86522b87f490025f39f0303b7db5bf5e3d8" +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + [[package]] name = "zeroize" version = "1.5.7" diff --git a/app/gui/language/span-tree/Cargo.toml b/app/gui/language/span-tree/Cargo.toml index 9c422a7a3b1b..0a8774d0d381 100644 --- a/app/gui/language/span-tree/Cargo.toml +++ b/app/gui/language/span-tree/Cargo.toml @@ -15,3 +15,4 @@ parser = { path = "../parser" } [dev-dependencies] wasm-bindgen-test = { workspace = true } +pretty_assertions = "1.4" diff --git a/app/gui/language/span-tree/src/builder.rs b/app/gui/language/span-tree/src/builder.rs index 9c398227c2ec..cdfa5150ff0e 100644 --- a/app/gui/language/span-tree/src/builder.rs +++ b/app/gui/language/span-tree/src/builder.rs @@ -97,4 +97,10 @@ impl ChildBuilder { self.built.node.ast_id = Some(id); self } + + /// Set tree type for this node. + pub fn set_tree_type(mut self, r#type: Option) -> Self { + self.built.node.tree_type = r#type; + self + } } diff --git a/app/gui/language/span-tree/src/generate.rs b/app/gui/language/span-tree/src/generate.rs index 5357e12c77e8..f1e7b262fa8f 100644 --- a/app/gui/language/span-tree/src/generate.rs +++ b/app/gui/language/span-tree/src/generate.rs @@ -985,6 +985,7 @@ mod test { use ast::Crumbs; use ast::IdMap; use parser::Parser; + use pretty_assertions::assert_eq; /// A helper function which removes information about expression id from thw tree rooted at @@ -1158,7 +1159,18 @@ mod test { let expected = TreeBuilder::new(13) .add_leaf(0, 3, node::Kind::Operation, PrefixCrumb::Func) .add_empty_child(3, BeforeArgument(0)) - .add_leaf(4, 9, node::Kind::prefix_argument(), PrefixCrumb::Arg) + .add_child(4, 9, node::Kind::prefix_argument(), PrefixCrumb::Arg) + .set_tree_type(Some(ast::TreeType::Lambda)) + .add_leaf(0, 3, node::Kind::Token, TreeCrumb { index: 0 }) + .add_child(4, 5, node::Kind::argument(), TreeCrumb { index: 3 }) + .add_empty_child(0, BeforeArgument(0)) + .add_leaf(0, 1, node::Kind::argument(), InfixCrumb::LeftOperand) + .add_leaf(2, 1, node::Kind::Operation, InfixCrumb::Operator) + .add_empty_child(3, BeforeArgument(1)) + .add_leaf(4, 1, node::Kind::argument(), InfixCrumb::RightOperand) + .add_empty_child(5, Append) + .done() + .done() .add_empty_child(13, Append) .build(); From e30b85400bc2fa64d0c24658a2786ce0334829db Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Thu, 10 Aug 2023 14:50:56 +0400 Subject: [PATCH 2/7] Draft implementation --- app/gui/language/span-tree/src/generate.rs | 78 ++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/app/gui/language/span-tree/src/generate.rs b/app/gui/language/span-tree/src/generate.rs index f1e7b262fa8f..dcb3456a13b1 100644 --- a/app/gui/language/span-tree/src/generate.rs +++ b/app/gui/language/span-tree/src/generate.rs @@ -344,6 +344,8 @@ fn generate_node_for_ast( match ast.shape() { ast::Shape::Prefix(_) => ast::prefix::Chain::from_ast(ast).unwrap().generate_node(kind, context), + ast::Shape::Tree(tree) if tree.type_info == ast::TreeType::Lambda => + lambda_generate_node(tree, kind, context, ast.id), ast::Shape::Tree(tree) if tree.type_info != ast::TreeType::Lambda => tree_generate_node(tree, kind, context, ast.id), ast::Shape::Block(block) => block_generate_node(block, kind, context, ast.id), @@ -824,6 +826,82 @@ fn generate_trailing_expected_arguments( // === SpanTree for Tree === // ========================= +fn lambda_generate_node( + tree: &ast::Tree, + kind: node::Kind, + context: &impl Context, + ast_id: Option, +) -> FallibleResult { + let mut children = vec![]; + let size; + if let Some(leaf_info) = &tree.leaf_info { + size = ByteDiff::from(leaf_info.len()); + } else { + let mut parent_offset = ByteDiff::from(0); + let mut sibling_offset = ByteDiff::from(0); + let is_arrow = |span_info| matches!(span_info, SpanSeed::Token(ast::SpanSeedToken { token }) if token == "->"); + let arrow_index = tree.span_info.iter().cloned().position(is_arrow).unwrap_or(0); + let mut is_in_body = false; + for (index, raw_span_info) in tree.span_info.iter().enumerate() { + if index > arrow_index && !is_in_body { + is_in_body = true; + let kind = node::Kind::Token; + let node = Node::new().with_kind(kind).with_size(parent_offset); + let ast_crumbs = vec![TreeCrumb { index: 0 }.into()]; + children.push(node::Child { + node, + parent_offset: ByteDiff::from(0), + sibling_offset: ByteDiff::from(0), + ast_crumbs, + }); + sibling_offset = 0.byte_diff(); + } + match raw_span_info { + SpanSeed::Space(ast::SpanSeedSpace { space }) => { + parent_offset += ByteDiff::from(space); + sibling_offset += ByteDiff::from(space); + } + SpanSeed::Token(ast::SpanSeedToken { token }) => { + let kind = node::Kind::Token; + let size = ByteDiff::from(token.len()); + let ast_crumbs = vec![TreeCrumb { index }.into()]; + let node = Node::new().with_kind(kind).with_size(size); + if is_in_body { + children.push(node::Child { + node, + parent_offset, + sibling_offset, + ast_crumbs, + }); + } + parent_offset += size; + sibling_offset = 0.byte_diff(); + } + SpanSeed::Child(ast::SpanSeedChild { node }) => { + let kind = node::Kind::argument().with_removable(false); + let node = node.generate_node(kind, context)?; + let child_size = node.size; + let ast_crumbs = vec![TreeCrumb { index }.into()]; + if is_in_body { + children.push(node::Child { + node, + parent_offset, + sibling_offset, + ast_crumbs, + }); + } + parent_offset += child_size; + sibling_offset = 0.byte_diff(); + } + } + } + size = parent_offset; + } + + let tree_type = Some(tree.type_info.clone()); + Ok(Node { kind, tree_type, size, children, ..default() }.with_ast_id(ast_id)) +} + fn tree_generate_node( tree: &ast::Tree, kind: node::Kind, From f327649970892406123b64ccf800cf3e1436a40c Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Thu, 10 Aug 2023 16:04:38 +0400 Subject: [PATCH 3/7] Refactoring --- app/gui/language/span-tree/src/generate.rs | 138 +++++++++------------ 1 file changed, 57 insertions(+), 81 deletions(-) diff --git a/app/gui/language/span-tree/src/generate.rs b/app/gui/language/span-tree/src/generate.rs index dcb3456a13b1..02a76753eeec 100644 --- a/app/gui/language/span-tree/src/generate.rs +++ b/app/gui/language/span-tree/src/generate.rs @@ -344,10 +344,7 @@ fn generate_node_for_ast( match ast.shape() { ast::Shape::Prefix(_) => ast::prefix::Chain::from_ast(ast).unwrap().generate_node(kind, context), - ast::Shape::Tree(tree) if tree.type_info == ast::TreeType::Lambda => - lambda_generate_node(tree, kind, context, ast.id), - ast::Shape::Tree(tree) if tree.type_info != ast::TreeType::Lambda => - tree_generate_node(tree, kind, context, ast.id), + ast::Shape::Tree(tree) => tree_generate_node(tree, kind, context, ast.id), ast::Shape::Block(block) => block_generate_node(block, kind, context, ast.id), _ => { let size = (ast.repr_len().value as i32).byte_diff(); @@ -820,86 +817,52 @@ fn generate_trailing_expected_arguments( }) } +/// A single child node produced out of the lambda argument and the `->` token. +#[derive(Debug)] +struct FoldedLambdaArguments { + /// Both the lambda argument and the `->` token, as a single [`node::Kind::Token`] node. + child: node::Child, + /// The number of tree nodes that were folded into the `child`. + nodes_replaced: usize, +} - -// ========================= -// === SpanTree for Tree === -// ========================= - -fn lambda_generate_node( +/// Fold the lambda arguments into a single [`node::Kind::Token`] node. +/// It is needed to ignore lambda arguments as connection targets, but still generate a valid +/// SpanTree from the lambda body. +fn fold_lambda_arguments( tree: &ast::Tree, - kind: node::Kind, context: &impl Context, - ast_id: Option, -) -> FallibleResult { - let mut children = vec![]; - let size; - if let Some(leaf_info) = &tree.leaf_info { - size = ByteDiff::from(leaf_info.len()); - } else { - let mut parent_offset = ByteDiff::from(0); - let mut sibling_offset = ByteDiff::from(0); - let is_arrow = |span_info| matches!(span_info, SpanSeed::Token(ast::SpanSeedToken { token }) if token == "->"); - let arrow_index = tree.span_info.iter().cloned().position(is_arrow).unwrap_or(0); - let mut is_in_body = false; - for (index, raw_span_info) in tree.span_info.iter().enumerate() { - if index > arrow_index && !is_in_body { - is_in_body = true; - let kind = node::Kind::Token; - let node = Node::new().with_kind(kind).with_size(parent_offset); - let ast_crumbs = vec![TreeCrumb { index: 0 }.into()]; - children.push(node::Child { - node, - parent_offset: ByteDiff::from(0), - sibling_offset: ByteDiff::from(0), - ast_crumbs, - }); - sibling_offset = 0.byte_diff(); - } - match raw_span_info { - SpanSeed::Space(ast::SpanSeedSpace { space }) => { - parent_offset += ByteDiff::from(space); - sibling_offset += ByteDiff::from(space); - } - SpanSeed::Token(ast::SpanSeedToken { token }) => { - let kind = node::Kind::Token; - let size = ByteDiff::from(token.len()); - let ast_crumbs = vec![TreeCrumb { index }.into()]; - let node = Node::new().with_kind(kind).with_size(size); - if is_in_body { - children.push(node::Child { - node, - parent_offset, - sibling_offset, - ast_crumbs, - }); - } - parent_offset += size; - sibling_offset = 0.byte_diff(); - } - SpanSeed::Child(ast::SpanSeedChild { node }) => { - let kind = node::Kind::argument().with_removable(false); - let node = node.generate_node(kind, context)?; - let child_size = node.size; - let ast_crumbs = vec![TreeCrumb { index }.into()]; - if is_in_body { - children.push(node::Child { - node, - parent_offset, - sibling_offset, - ast_crumbs, - }); - } - parent_offset += child_size; - sibling_offset = 0.byte_diff(); - } +) -> Result { + let is_arrow = |span_info| matches!(span_info, SpanSeed::Token(ast::SpanSeedToken { token }) if token == "->"); + let arrow_index = tree.span_info.iter().cloned().position(is_arrow).unwrap_or(0); + let bytes_till_body = tree + .span_info + .iter() + .take(arrow_index + 1) + .map(|raw_span_info| match raw_span_info { + SpanSeed::Space(ast::SpanSeedSpace { space }) => Ok(*space), + SpanSeed::Token(ast::SpanSeedToken { token }) => Ok(token.len()), + SpanSeed::Child(ast::SpanSeedChild { node }) => { + let kind = node::Kind::argument().with_removable(false); + let node = node.generate_node(kind, context); + node.map(|node| node.size.as_usize()) } - } - size = parent_offset; - } - - let tree_type = Some(tree.type_info.clone()); - Ok(Node { kind, tree_type, size, children, ..default() }.with_ast_id(ast_id)) + }) + .collect::, _>>()? + .into_iter() + .sum::(); + let kind = node::Kind::Token; + let size = ByteDiff::from(bytes_till_body); + let node = Node::new().with_kind(kind).with_size(size); + let ast_crumbs = vec![TreeCrumb { index: 0 }.into()]; + let nodes_replaced = arrow_index + 1; + let child = node::Child { + node, + parent_offset: ByteDiff::from(0), + sibling_offset: ByteDiff::from(0), + ast_crumbs, + }; + Ok(FoldedLambdaArguments { child, nodes_replaced }) } fn tree_generate_node( @@ -924,7 +887,20 @@ fn tree_generate_node( let last_token_index = tree.span_info.iter().rposition(|span| matches!(span, SpanSeed::Token(_))); - for (index, raw_span_info) in tree.span_info.iter().enumerate() { + + // If the node is a lambda, we fold the lambda arguments into a single child node, + // and then continue handling the lambda body as usual. + let skip = if tree.type_info == ast::TreeType::Lambda { + let FoldedLambdaArguments { child, nodes_replaced } = + fold_lambda_arguments(tree, context)?; + parent_offset += child.node.size; + children.push(child); + nodes_replaced + } else { + 0 + }; + for (index, raw_span_info) in tree.span_info.iter().skip(skip).enumerate() { + let index = index + skip; match raw_span_info { SpanSeed::Space(ast::SpanSeedSpace { space }) => { parent_offset += ByteDiff::from(space); From d6e17050cf5adc40dc8dd7102342ced7a45bdb00 Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Thu, 10 Aug 2023 16:28:07 +0400 Subject: [PATCH 4/7] enso formatter --- app/gui/view/src/notification.rs | 1 - app/gui/view/src/notification/logged.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/app/gui/view/src/notification.rs b/app/gui/view/src/notification.rs index 39dfaf25048f..f217e393e20c 100644 --- a/app/gui/view/src/notification.rs +++ b/app/gui/view/src/notification.rs @@ -10,7 +10,6 @@ use crate::notification::logged::UpdateOptions; use ensogl::application::Application; - // ============== // === Export === // ============== diff --git a/app/gui/view/src/notification/logged.rs b/app/gui/view/src/notification/logged.rs index bcca1332fcbf..c2a316a4ae08 100644 --- a/app/gui/view/src/notification/logged.rs +++ b/app/gui/view/src/notification/logged.rs @@ -16,7 +16,6 @@ use crate::notification::js::HandleJsError; use uuid::Uuid; - // ============== // === Export === // ============== From d6b3d2e9c974328708276c5eb891de86203cef40 Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Thu, 10 Aug 2023 16:45:03 +0400 Subject: [PATCH 5/7] Add a changelog entry --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4ac4d776b08..bfdad000d013 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -215,6 +215,8 @@ the selected entry in the component browser. Clear separating lines between method arguments were added. The node selection was made easier with additional thick interactive selection border. +- [Connections to lamdas are displayed correctly][7550]. It is possible to drag + a connection to any expression inside the lambda body. [5910]: https://github.com/enso-org/enso/pull/5910 [6279]: https://github.com/enso-org/enso/pull/6279 @@ -239,6 +241,7 @@ [7372]: https://github.com/enso-org/enso/pull/7372 [7337]: https://github.com/enso-org/enso/pull/7337 [7311]: https://github.com/enso-org/enso/pull/7311 +[7550]: https://github.com/enso-org/enso/pull/7550 #### EnsoGL (rendering engine) From 9aa59253022becef82a7f9f04c9e931bea6de00d Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Thu, 10 Aug 2023 21:17:38 +0400 Subject: [PATCH 6/7] Do not generate span tree for skipped children --- app/gui/language/span-tree/src/generate.rs | 26 +++++++--------------- lib/rust/text/src/unit.rs | 7 ++++++ 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/app/gui/language/span-tree/src/generate.rs b/app/gui/language/span-tree/src/generate.rs index 02a76753eeec..628fb6aa0ebe 100644 --- a/app/gui/language/span-tree/src/generate.rs +++ b/app/gui/language/span-tree/src/generate.rs @@ -829,10 +829,7 @@ struct FoldedLambdaArguments { /// Fold the lambda arguments into a single [`node::Kind::Token`] node. /// It is needed to ignore lambda arguments as connection targets, but still generate a valid /// SpanTree from the lambda body. -fn fold_lambda_arguments( - tree: &ast::Tree, - context: &impl Context, -) -> Result { +fn fold_lambda_arguments(tree: &ast::Tree) -> FoldedLambdaArguments { let is_arrow = |span_info| matches!(span_info, SpanSeed::Token(ast::SpanSeedToken { token }) if token == "->"); let arrow_index = tree.span_info.iter().cloned().position(is_arrow).unwrap_or(0); let bytes_till_body = tree @@ -840,19 +837,13 @@ fn fold_lambda_arguments( .iter() .take(arrow_index + 1) .map(|raw_span_info| match raw_span_info { - SpanSeed::Space(ast::SpanSeedSpace { space }) => Ok(*space), - SpanSeed::Token(ast::SpanSeedToken { token }) => Ok(token.len()), - SpanSeed::Child(ast::SpanSeedChild { node }) => { - let kind = node::Kind::argument().with_removable(false); - let node = node.generate_node(kind, context); - node.map(|node| node.size.as_usize()) - } + SpanSeed::Space(ast::SpanSeedSpace { space }) => ByteDiff::from(space), + SpanSeed::Token(ast::SpanSeedToken { token }) => ByteDiff::from(token.len()), + SpanSeed::Child(ast::SpanSeedChild { node }) => node.repr_len().to_diff(), }) - .collect::, _>>()? - .into_iter() - .sum::(); + .sum::(); + let size = bytes_till_body; let kind = node::Kind::Token; - let size = ByteDiff::from(bytes_till_body); let node = Node::new().with_kind(kind).with_size(size); let ast_crumbs = vec![TreeCrumb { index: 0 }.into()]; let nodes_replaced = arrow_index + 1; @@ -862,7 +853,7 @@ fn fold_lambda_arguments( sibling_offset: ByteDiff::from(0), ast_crumbs, }; - Ok(FoldedLambdaArguments { child, nodes_replaced }) + FoldedLambdaArguments { child, nodes_replaced } } fn tree_generate_node( @@ -891,8 +882,7 @@ fn tree_generate_node( // If the node is a lambda, we fold the lambda arguments into a single child node, // and then continue handling the lambda body as usual. let skip = if tree.type_info == ast::TreeType::Lambda { - let FoldedLambdaArguments { child, nodes_replaced } = - fold_lambda_arguments(tree, context)?; + let FoldedLambdaArguments { child, nodes_replaced } = fold_lambda_arguments(tree); parent_offset += child.node.size; children.push(child); nodes_replaced diff --git a/lib/rust/text/src/unit.rs b/lib/rust/text/src/unit.rs index 84f95ea4d98e..5114239a7922 100644 --- a/lib/rust/text/src/unit.rs +++ b/lib/rust/text/src/unit.rs @@ -5,6 +5,7 @@ use crate::index::*; use crate::prelude::*; use enso_types::unit; +use std::iter::Sum; @@ -118,6 +119,12 @@ impl SubAssign for ByteDiff { } } +impl Sum for ByteDiff { + fn sum>(iter: I) -> Self { + iter.fold(ByteDiff(0), |acc, x| acc + x) + } +} + // ================ From 5afc8409438b7ffff75b768bde6dee7b8a8d9ab6 Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Thu, 10 Aug 2023 21:34:58 +0400 Subject: [PATCH 7/7] Add a test case for two-argument lambda --- app/gui/language/span-tree/src/generate.rs | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/app/gui/language/span-tree/src/generate.rs b/app/gui/language/span-tree/src/generate.rs index 628fb6aa0ebe..264a8cacaf20 100644 --- a/app/gui/language/span-tree/src/generate.rs +++ b/app/gui/language/span-tree/src/generate.rs @@ -1195,6 +1195,10 @@ mod test { #[test] fn generating_span_tree_for_lambda() { let parser = Parser::new(); + + + // === Simple lambda === + let ast = parser.parse_line_ast("foo a-> b + c").unwrap(); let mut tree: SpanTree = ast.generate_tree(&context::Empty).unwrap(); clear_expression_ids(&mut tree.root); @@ -1217,7 +1221,37 @@ mod test { .done() .add_empty_child(13, Append) .build(); + assert_eq!(expected, tree); + + + // === Lambda with two arguments === + + let ast = parser.parse_line_ast("foo a->b-> a + b").unwrap(); + let mut tree: SpanTree = ast.generate_tree(&context::Empty).unwrap(); + clear_expression_ids(&mut tree.root); + clear_parameter_infos(&mut tree.root); + let expected = TreeBuilder::new(16) + .add_leaf(0, 3, node::Kind::Operation, PrefixCrumb::Func) + .add_empty_child(3, BeforeArgument(0)) + .add_child(4, 12, node::Kind::prefix_argument(), PrefixCrumb::Arg) + .set_tree_type(Some(ast::TreeType::Lambda)) + .add_leaf(0, 3, node::Kind::Token, TreeCrumb { index: 0 }) + .add_child(3, 9, node::Kind::argument(), TreeCrumb { index: 2 }) + .set_tree_type(Some(ast::TreeType::Lambda)) + .add_leaf(0, 3, node::Kind::Token, TreeCrumb { index: 0 }) + .add_child(4, 5, node::Kind::argument(), TreeCrumb { index: 3 }) + .add_empty_child(0, BeforeArgument(0)) + .add_leaf(0, 1, node::Kind::argument(), InfixCrumb::LeftOperand) + .add_leaf(2, 1, node::Kind::Operation, InfixCrumb::Operator) + .add_empty_child(3, BeforeArgument(1)) + .add_leaf(4, 1, node::Kind::argument(), InfixCrumb::RightOperand) + .add_empty_child(5, Append) + .done() + .done() + .done() + .add_empty_child(16, Append) + .build(); assert_eq!(expected, tree); }