Skip to content

Commit

Permalink
Widgets integrated with graph nodes (#6347)
Browse files Browse the repository at this point in the history
Rewrites node input component. Now the input is composed of multiple widget components arranged in a tree of views with automatic layout. That allows creating complex UI elements on top of the node itself, and further widget positions will be automatically adapted to that. The tree roughly follow the span tree, as it is built by consuming its nodes and eagerly creating widgets from them. The tree is rebuilt every time the expression changes, but that rebuild process reuses as much previously created widgets as possible, and only updates their configuration as needed. Each widget type can have its own configuration options that can be passed to it from the parent, or assigned based on configuration received from the language server.

<img width="773" alt="image" src="https://user-images.githubusercontent.com/919491/233439310-9c39ea88-19bc-43da-9baf-1bb176e2724e.png">

# Important Notes
For now, all span-tree updates are sent over to the shared Frp endpoint of the whole tree, so there is no mechanism for intercepting them by the parent widgets. One idea would be to use existing bubbling/capturing events on widget display objects for that purpose, but I think existing implementation is simpler and more convenient, and we can always easily change that if we have a use for it.

There are some issues with performance due to much more display objects being created on the graph. Expect it to be a little worse, especially at initialization time.
  • Loading branch information
Frizi authored Apr 26, 2023
1 parent 0e51131 commit a00efb2
Show file tree
Hide file tree
Showing 41 changed files with 3,917 additions and 2,011 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@
quickly understand each button's function.
- [File associations are created on Windows and macOS][6077]. This allows
opening Enso files by double-clicking them in the file explorer.
- [Added capability to create node widgets with complex UI][6347]. Node widgets
such as dropdown can now be placed in the node and affect the code text flow.
- [The IDE UI element for selecting the execution mode of the project is now
sending messages to the backend.][6341].

Expand Down Expand Up @@ -596,6 +598,7 @@
[6294]: https://github.com/enso-org/enso/pull/6294
[6383]: https://github.com/enso-org/enso/pull/6383
[6404]: https://github.com/enso-org/enso/pull/6404
[6347]: https://github.com/enso-org/enso/pull/6347

#### Enso Compiler

Expand Down
12 changes: 6 additions & 6 deletions app/gui/language/span-tree/src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ use ast::Ast;



/// ==============
/// === Errors ===
/// ==============
// ==============
// === Errors ===
// ==============

/// Error returned when tried to perform an action which is not available for specific SpanTree
/// node.
Expand All @@ -35,9 +35,9 @@ pub struct AstSpanTreeMismatch;



/// =====================
/// === Actions Trait ===
/// =====================
// =====================
// === Actions Trait ===
// =====================

/// Action enum used mainly for error messages.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
Expand Down
30 changes: 16 additions & 14 deletions app/gui/language/span-tree/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,23 @@ pub trait Builder<T: Payload>: Sized {
/// Add new AST-type child to node. Returns the child's builder which may be used to further
/// extend this branch of the tree.
fn add_child(
self,
offset: usize,
mut self,
parent_offset: usize,
len: usize,
kind: impl Into<node::Kind>,
crumbs: impl IntoCrumbs,
) -> ChildBuilder<Self, T> {
let kind = kind.into();
let node = Node::<T>::new().with_kind(kind).with_size(len.into());
let child = node::Child { node, offset: offset.into(), ast_crumbs: crumbs.into_crumbs() };
let prev_child = self.node_being_built().children.last();
let prev_child_end = prev_child.map_or(0, |c| (c.parent_offset + c.node.size).as_usize());
let sibling_offset = parent_offset.saturating_sub(prev_child_end);
let child = node::Child {
node,
parent_offset: parent_offset.into(),
sibling_offset: sibling_offset.into(),
ast_crumbs: crumbs.into_crumbs(),
};
ChildBuilder { built: child, parent: self }
}

Expand All @@ -46,14 +54,8 @@ pub trait Builder<T: Payload>: Sized {
}

/// Add an Empty-type child to node.
fn add_empty_child(mut self, offset: usize, kind: impl Into<node::Kind>) -> Self {
let child = node::Child {
node: Node::<T>::new().with_kind(kind),
offset: offset.into(),
ast_crumbs: vec![],
};
self.node_being_built().children.push(child);
self
fn add_empty_child(self, offset: usize, kind: impl Into<node::Kind>) -> Self {
self.add_leaf(offset, 0, kind, ast::crumbs![])
}

/// Set expression id for this node.
Expand All @@ -65,9 +67,9 @@ pub trait Builder<T: Payload>: Sized {



/// ================
/// === Builders ===
/// ================
// ================
// === Builders ===
// ================

// === SpanTree Builder ===

Expand Down
54 changes: 33 additions & 21 deletions app/gui/language/span-tree/src/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,17 @@ impl<T: Payload> SpanTreeGenerator<T> for String {
#[derivative(Default(bound = ""))]
struct ChildGenerator<T> {
current_offset: ByteDiff,
sibling_offset: ByteDiff,
children: Vec<node::Child<T>>,
}

impl<T: Payload> ChildGenerator<T> {
/// Add spacing to current generator state. It will be taken into account for the next generated
/// children's offsets
fn spacing(&mut self, size: usize) {
self.current_offset += (size as i32).byte_diff();
let offset = (size as i32).byte_diff();
self.current_offset += offset;
self.sibling_offset += offset;
}

fn generate_ast_node(
Expand All @@ -115,27 +118,26 @@ impl<T: Payload> ChildGenerator<T> {
}

fn add_node(&mut self, ast_crumbs: ast::Crumbs, node: Node<T>) -> &mut node::Child<T> {
let offset = self.current_offset;
let child = node::Child { node, offset, ast_crumbs };
let parent_offset = self.current_offset;
let sibling_offset = self.sibling_offset;
let child = node::Child { node, parent_offset, sibling_offset, ast_crumbs };
self.current_offset += child.node.size;
self.sibling_offset = 0.byte_diff();
self.children.push(child);
self.children.last_mut().unwrap()
}

fn generate_empty_node(&mut self, insert_type: InsertionPointType) -> &mut node::Child<T> {
let child = node::Child {
node: Node::<T>::new().with_kind(insert_type),
offset: self.current_offset,
ast_crumbs: vec![],
};
self.children.push(child);
self.children.last_mut().unwrap()
self.add_node(vec![], Node::<T>::new().with_kind(insert_type))
}

fn reverse_children(&mut self) {
self.children.reverse();
let mut last_parent_offset = 0.byte_diff();
for child in &mut self.children {
child.offset = self.current_offset - child.offset - child.node.size;
child.parent_offset = self.current_offset - child.parent_offset - child.node.size;
child.sibling_offset = child.parent_offset - last_parent_offset;
last_parent_offset = child.parent_offset;
}
}

Expand All @@ -149,9 +151,9 @@ impl<T: Payload> ChildGenerator<T> {



/// =============================
/// === Trait Implementations ===
/// =============================
// =============================
// === Trait Implementations ===
// =============================

/// Helper structure constructed from Ast that consists base of prefix application.
///
Expand Down Expand Up @@ -528,6 +530,10 @@ fn generate_node_for_prefix_chain<T: Payload>(
context: &impl Context,
) -> FallibleResult<Node<T>> {
let app_base = ApplicationBase::from_prefix_chain(this);

// If actual method arguments are not resolved, we still want to assign correct call ID to all
// argument spans. This is required for correct handling of span tree actions, as it is used to
// determine correct reinsertion point for removed span.
let fallback_call_id = app_base.call_id;
let mut application = app_base.resolve(context);

Expand Down Expand Up @@ -811,29 +817,35 @@ fn tree_generate_node<T: Payload>(
if let Some(leaf_info) = &tree.leaf_info {
size = ByteDiff::from(leaf_info.len());
} else {
let mut offset = ByteDiff::from(0);
let mut parent_offset = ByteDiff::from(0);
let mut sibling_offset = ByteDiff::from(0);
for (index, raw_span_info) in tree.span_info.iter().enumerate() {
match raw_span_info {
SpanSeed::Space(ast::SpanSeedSpace { space }) => offset += ByteDiff::from(space),
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 { kind, size, ..default() };
children.push(node::Child { node, offset, ast_crumbs });
offset += size;
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();
let node = node.generate_node(kind, context)?;
let child_size = node.size;
let ast_crumbs = vec![TreeCrumb { index }.into()];
children.push(node::Child { node, offset, ast_crumbs });
offset += child_size;
children.push(node::Child { node, parent_offset, sibling_offset, ast_crumbs });
parent_offset += child_size;
sibling_offset = 0.byte_diff();
}
}
}
size = offset;
size = parent_offset;
}
let payload = default();
Ok(Node { kind, parenthesized, size, children, ast_id, payload })
Expand Down
72 changes: 34 additions & 38 deletions app/gui/language/span-tree/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,32 +224,32 @@ impl<T> SpanTree<T> {
///
/// Example output with AST ids removed for clarity:
/// ```text
/// operator6.join operator31 Join_Kind.Inner ["County"] Root
/// operator6.join operator31 Join_Kind.Inner ["County"] ├─Chained
/// operator6.join operator31 Join_Kind.Inner ["County"] │ ├── Chained
/// operator6.join operator31 Join_Kind.Inner │ ├── Chained
/// operator6.join operator31 ├── Chained
/// operator6.join ├── Operation
/// ├── InsertionPoint(BeforeTarget)
/// operator6 ├── This
/// ├── InsertionPoint(AfterTarget)
/// . ├── Operation
/// join ├── Argument
/// ╰── InsertionPoint(Append)
/// operator31 ╰── Argument name="right"
/// Join_Kind.Inner │ ╰── Argument name="join_kind"
/// ├── InsertionPoint(BeforeTarget)
/// Join_Kind │ ├── This
/// ├── InsertionPoint(AfterTarget)
/// . ├── Operation
/// Inner │ ├── Argument
/// ╰── InsertionPoint(Append)
/// ["County"] │ ╰── Argument name="on"
/// [ ├── Token
/// "County" │ ├── Argument
/// ] ╰── Token
/// ╰── InsertionPoint(ExpectedArgument(3)) name="right_prefix"
/// ▲╰── InsertionPoint(ExpectedArgument(4)) name="on_problems"
/// ▷operator4.join operator2 Join_Kind.Inner ["County"]Root
/// ▷operator4.join operator2 Join_Kind.Inner ["County"] ├─Chained
/// ▷operator4.join operator2 Join_Kind.Inner ["County"]├─Chained
/// ▷operator4.join operator2 Join_Kind.Inner │ │ ├─Chained
/// ▷operator4.join operator2◁ │ │ │ ├─Chained
/// ▷operator4.join │ │ │ │ ├─Operation
/// ▷◁ │ │ │ │ │ ├─InsertionPoint(BeforeArgument(0))
/// ▷operator4◁ │ │ │ │ │ ├─Argument name="self"
/// ▷.◁ │ │ │ │ │ ├─Operation
/// ▷◁ │ │ │ │ │ ├─InsertionPoint(BeforeArgument(1))
/// join │ │ │ │ │ ├─Argument
/// ▷◁ │ │ │ │ │ ╰─InsertionPoint(Append)
/// ▷operator2◁ │ │ │ │ ╰─Argument name="right"
/// Join_Kind.Inner │ │ │ ╰─Argument name="join_kind"
/// ▷◁ │ │ │ ├─InsertionPoint(BeforeArgument(0))
/// Join_Kind │ │ │ ├─Argument
/// ▷.◁ │ │ │ ├─Operation
/// ▷◁ │ │ │ ├─InsertionPoint(BeforeArgument(1))
/// Inner │ │ │ ├─Argument
/// ▷◁ │ │ │ ╰─InsertionPoint(Append)
/// ["County"] │ │ ╰─Argument name="on"
/// ▷[◁ │ │ ├─Token
/// "County" │ │ ├─Argument
/// ▷]◁ │ │ ╰─Token
/// ▷◁ ╰─InsertionPoint(ExpectedArgument) name="right_prefix"
/// ▷◁ ╰─InsertionPoint(ExpectedArgument) name="on_problems"
/// ```
pub fn debug_print(&self, code: &str) -> String {
use std::fmt::Write;
Expand All @@ -261,7 +261,7 @@ impl<T> SpanTree<T> {
}

let mut buffer = String::new();
let span_padding = " ".repeat(code.len() + 1);
let span_padding = " ".repeat(code.len() + 2);

struct PrintState {
indent: String,
Expand All @@ -271,21 +271,17 @@ impl<T> SpanTree<T> {
self.root_ref().dfs_with_layer_data(state, |node, state| {
let span = node.span();
let node_code = &code[span];
buffer.push_str(&span_padding[0..node.span_offset.into()]);
let mut written = node.span_offset.into();
if node_code.is_empty() {
buffer.push('▲');
written += 1;
} else {
buffer.push_str(node_code);
written += node_code.len();
}
buffer.push_str(&span_padding[0..node.span_offset.value]);
buffer.push('▷');
buffer.push_str(node_code);
buffer.push('◁');
let written = node.span_offset.value + node_code.len() + 2;
buffer.push_str(&span_padding[written..]);

let indent = if let Some(index) = node.crumbs.last() {
let is_last = *index == state.num_children - 1;
let indent_targeted = if is_last { "╰── " } else { "├── " };
let indent_continue = if is_last { " " } else { " " };
let indent_targeted = if is_last { " ╰─" } else { " ├─" };
let indent_continue = if is_last { " " } else { " " };

buffer.push_str(&state.indent);
buffer.push_str(indent_targeted);
Expand Down
Loading

0 comments on commit a00efb2

Please sign in to comment.