Virtual Component Groups in the Hierarchical Action List (1/2) (#3488)

Parse the Engine's response containing Virtual Component Groups and store the results in a field of the Execution Context type.

https://www.pivotaltracker.com/story/show/181865548

# Important Notes
- This PR implements the subtask 1 of 2 in the ["Virtual Component Groups in the Hierarchical Action List" task](https://www.pivotaltracker.com/story/show/181865548).

[ci no changelog needed]
This commit is contained in:
Mateusz Czapliński 2022-06-03 19:18:20 +02:00 committed by GitHub
parent 66693ad642
commit 656d6e7660
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 277 additions and 60 deletions

3
Cargo.lock generated
View File

@ -2042,6 +2042,7 @@ name = "enso-prelude"
version = "0.2.6"
dependencies = [
"anyhow",
"assert_approx_eq",
"backtrace",
"boolinator",
"cfg-if 1.0.0",
@ -2186,7 +2187,6 @@ dependencies = [
name = "enso-types"
version = "0.1.0"
dependencies = [
"assert_approx_eq",
"nalgebra 0.26.2",
"num-traits",
"paste 1.0.7",
@ -2252,7 +2252,6 @@ name = "ensogl-core"
version = "0.1.0"
dependencies = [
"Inflector",
"assert_approx_eq",
"bit_field",
"code-builder",
"console_error_panic_hook",

View File

@ -1036,8 +1036,8 @@ pub struct SuggestionDatabaseUpdatesEvent {
#[serde(rename_all = "camelCase")]
#[allow(missing_docs)]
pub struct LibraryComponent {
name: String,
shortcut: Option<String>,
pub name: String,
pub shortcut: Option<String>,
}
/// The component group provided by a library.
@ -1045,16 +1045,16 @@ pub struct LibraryComponent {
#[serde(rename_all = "camelCase")]
#[allow(missing_docs)]
pub struct LibraryComponentGroup {
/// The fully qualified module name. A string consisting of a namespace and a library name
/// The fully qualified library name. A string consisting of a namespace and a library name
/// separated by the dot <namespace>.<library name>, i.e. `Standard.Base`
library: String,
pub library: String,
/// The group name without the library name prefix. E.g. given the `Standard.Base.Group 1`
/// group reference, the `group` field contains `Group 1`.
group: String,
color: Option<String>,
icon: Option<String>,
/// group reference, the `name` field contains `Group 1`.
pub name: String,
pub color: Option<String>,
pub icon: Option<String>,
/// The list of components provided by this component group.
exports: Vec<LibraryComponent>,
pub exports: Vec<LibraryComponent>,
}

View File

@ -3,6 +3,7 @@
use crate::prelude::*;
use crate::model::module::QualifiedName as ModuleQualifiedName;
use crate::model::suggestion_database::entry as suggestion;
use crate::notification::Publisher;
use engine_protocol::language_server;
@ -11,6 +12,7 @@ use engine_protocol::language_server::ExpressionUpdatePayload;
use engine_protocol::language_server::MethodPointer;
use engine_protocol::language_server::SuggestionId;
use engine_protocol::language_server::VisualisationConfiguration;
use ensogl::data::color;
use flo_stream::Subscriber;
use mockall::automock;
use serde::Deserialize;
@ -272,6 +274,45 @@ pub struct AttachedVisualization {
// ======================
// === ComponentGroup ===
// ======================
/// A named group of components which is defined in a library imported into an execution context.
///
/// Components are language elements displayed by the Component Browser. The Component Browser
/// displays them in groups defined in libraries imported into an execution context.
/// To learn more about component groups, see the [Component Browser Design
/// Document](https://github.com/enso-org/design/blob/e6cffec2dd6d16688164f04a4ef0d9dff998c3e7/epics/component-browser/design.md).
#[allow(missing_docs)]
#[derive(Clone, Debug, PartialEq)]
pub struct ComponentGroup {
pub name: ImString,
/// An optional color to use when displaying the component group.
pub color: Option<color::Rgb>,
pub components: Vec<suggestion::QualifiedName>,
}
impl ComponentGroup {
/// Construct from a [`language_server::LibraryComponentGroup`].
pub fn from_language_server_protocol_struct(
group: language_server::LibraryComponentGroup,
) -> Self {
let name = group.name.into();
let color = group.color.as_ref().and_then(|c| color::Rgb::from_css_hex(c));
let components = group.exports.into_iter().map(|e| e.name.into()).collect();
ComponentGroup { name, color, components }
}
}
impl From<language_server::LibraryComponentGroup> for ComponentGroup {
fn from(group: language_server::LibraryComponentGroup) -> Self {
Self::from_language_server_protocol_struct(group)
}
}
// =============
// === Model ===
// =============

View File

@ -3,6 +3,7 @@
use crate::prelude::*;
use crate::model::execution_context::AttachedVisualization;
use crate::model::execution_context::ComponentGroup;
use crate::model::execution_context::ComputedValueInfoRegistry;
use crate::model::execution_context::LocalCall;
use crate::model::execution_context::Visualization;
@ -59,6 +60,8 @@ pub struct ExecutionContext {
pub computed_value_info_registry: Rc<ComputedValueInfoRegistry>,
/// Execution context is considered ready once it completes it first execution after creation.
pub is_ready: crate::sync::Synchronized<bool>,
/// Component groups defined in libraries imported into the execution context.
pub component_groups: RefCell<Vec<ComponentGroup>>,
}
impl ExecutionContext {
@ -69,7 +72,16 @@ impl ExecutionContext {
let visualizations = default();
let computed_value_info_registry = default();
let is_ready = default();
Self { logger, entry_point, stack, visualizations, computed_value_info_registry, is_ready }
let component_groups = default();
Self {
logger,
entry_point,
stack,
visualizations,
computed_value_info_registry,
is_ready,
component_groups,
}
}
/// Creates a `VisualisationConfiguration` for the visualization with given id. It may be used
@ -247,15 +259,17 @@ pub mod test {
use double_representation::definition::DefinitionName;
use double_representation::project;
use engine_protocol::language_server;
#[derive(Clone, Derivative)]
#[derivative(Debug)]
pub struct MockData {
pub module_path: model::module::Path,
pub context_id: model::execution_context::Id,
pub root_definition: DefinitionName,
pub namespace: String,
pub project_name: String,
pub module_path: model::module::Path,
pub context_id: model::execution_context::Id,
pub root_definition: DefinitionName,
pub namespace: String,
pub project_name: String,
pub component_groups: Vec<language_server::LibraryComponentGroup>,
}
impl Default for MockData {
@ -267,11 +281,12 @@ pub mod test {
impl MockData {
pub fn new() -> MockData {
MockData {
context_id: model::execution_context::Id::new_v4(),
module_path: crate::test::mock::data::module_path(),
root_definition: crate::test::mock::data::definition_name(),
namespace: crate::test::mock::data::NAMESPACE_NAME.to_owned(),
project_name: crate::test::mock::data::PROJECT_NAME.to_owned(),
context_id: model::execution_context::Id::new_v4(),
module_path: crate::test::mock::data::module_path(),
root_definition: crate::test::mock::data::definition_name(),
namespace: crate::test::mock::data::NAMESPACE_NAME.to_owned(),
project_name: crate::test::mock::data::PROJECT_NAME.to_owned(),
component_groups: vec![],
}
}

View File

@ -77,6 +77,16 @@ impl ExecutionContext {
let this = Self { id, model, language_server, logger };
this.push_root_frame().await?;
info!(this.logger, "Pushed root frame.");
match this.load_component_groups().await {
Ok(_) => info!(this.logger, "Loaded component groups."),
Err(err) => {
let msg = iformat!(
"Failed to load component groups. No groups will appear in the Favorites \
section of the Component Browser. Error: {err}"
);
error!(this.logger, "{msg}");
}
}
Ok(this)
}
}
@ -96,6 +106,14 @@ impl ExecutionContext {
result.map(|res| res.map_err(|err| err.into()))
}
/// Load the component groups defined in libraries imported into the execution context.
async fn load_component_groups(&self) -> FallibleResult {
let ls_response = self.language_server.get_component_groups(&self.id).await?;
*self.model.component_groups.borrow_mut() =
ls_response.component_groups.into_iter().map(|group| group.into()).collect();
Ok(())
}
/// Detach visualization from current execution context.
///
/// Necessary because the Language Server requires passing both visualization ID and expression
@ -280,10 +298,11 @@ pub mod test {
use crate::executor::test_utils::TestWithLocalPoolExecutor;
use crate::model::execution_context::plain::test::MockData;
use crate::model::execution_context::ComponentGroup;
use crate::model::module::QualifiedName;
use crate::model::traits::*;
use engine_protocol::language_server::response::CreateExecutionContext;
use engine_protocol::language_server::response;
use engine_protocol::language_server::CapabilityRegistration;
use engine_protocol::language_server::ExpressionUpdates;
use json_rpc::expect_call;
@ -303,9 +322,15 @@ pub mod test {
fn new_customized(
ls_setup: impl FnOnce(&mut language_server::MockClient, &MockData),
) -> Fixture {
let data = MockData::new();
Self::new_customized_with_data(MockData::new(), ls_setup)
}
fn new_customized_with_data(
data: MockData,
ls_setup: impl FnOnce(&mut language_server::MockClient, &MockData),
) -> Fixture {
let mut ls_client = language_server::MockClient::default();
Self::mock_create_push_destroy_calls(&data, &mut ls_client);
Self::mock_default_calls(&data, &mut ls_client);
ls_setup(&mut ls_client, &data);
ls_client.require_all_calls();
let connection = language_server::Connection::new_mock_rc(ls_client);
@ -318,13 +343,13 @@ pub mod test {
}
/// What is expected server's response to a successful creation of this context.
fn expected_creation_response(data: &MockData) -> CreateExecutionContext {
fn expected_creation_response(data: &MockData) -> response::CreateExecutionContext {
let context_id = data.context_id;
let can_modify =
CapabilityRegistration::create_can_modify_execution_context(context_id);
let receives_updates =
CapabilityRegistration::create_receives_execution_context_updates(context_id);
CreateExecutionContext { context_id, can_modify, receives_updates }
response::CreateExecutionContext { context_id, can_modify, receives_updates }
}
/// Sets up mock client expectations for context creation and destruction.
@ -335,12 +360,9 @@ pub mod test {
expect_call!(ls.destroy_execution_context(id) => Ok(()));
}
/// Sets up mock client expectations for context creation, initial frame push
/// and destruction.
pub fn mock_create_push_destroy_calls(
data: &MockData,
ls: &mut language_server::MockClient,
) {
/// Sets up mock client expectations for all the calls issued by default by the
/// [`ExecutionContext::create`] and [`ExecutionContext::drop`] methods.
pub fn mock_default_calls(data: &MockData, ls: &mut language_server::MockClient) {
Self::mock_create_destroy_calls(data, ls);
let id = data.context_id;
let root_frame = language_server::ExplicitCall {
@ -350,6 +372,10 @@ pub mod test {
};
let stack_item = language_server::StackItem::ExplicitCall(root_frame);
expect_call!(ls.push_to_execution_context(id,stack_item) => Ok(()));
let component_groups = language_server::response::GetComponentGroups {
component_groups: data.component_groups.clone(),
};
expect_call!(ls.get_component_groups(id) => Ok(component_groups));
}
/// Generates a mock update for a random expression id.
@ -513,4 +539,68 @@ pub mod test {
context.modify_visualization(vis_id, expression, module).await.unwrap();
});
}
/// Check that the [`ExecutionContext::load_component_groups`] method correctly parses
/// a mocked Language Server response and loads the result into a field of the
/// [`ExecutionContext`].
#[test]
fn loading_component_groups() {
// Prepare sample component groups to be returned by a mock Language Server client.
fn library_component(name: &str) -> language_server::LibraryComponent {
language_server::LibraryComponent { name: name.to_string(), shortcut: None }
}
let sample_ls_component_groups = vec![
// A sample component group in local namespace, with non-empty color, and with exports
// from the local namespace as well as from the standard library.
language_server::LibraryComponentGroup {
library: "local.Unnamed_10".to_string(),
name: "Test Group 1".to_string(),
color: Some("#C047AB".to_string()),
icon: None,
exports: vec![
library_component("Standard.Base.System.File.new"),
library_component("local.Unnamed_10.Main.main"),
],
},
// A sample component group from the standard library, without a predefined color.
language_server::LibraryComponentGroup {
library: "Standard.Base".to_string(),
name: "Input".to_string(),
color: None,
icon: None,
exports: vec![library_component("Standard.Base.System.File.new")],
},
];
// Create a test fixture based on the sample data.
let mut mock_data = MockData::new();
mock_data.component_groups = sample_ls_component_groups;
let fixture = Fixture::new_customized_with_data(mock_data, |_, _| {});
let Fixture { mut test, context, .. } = fixture;
// Run a test and verify that the sample component groups were parsed correctly and have
// expected contents.
test.run_task(async move {
let groups = context.model.component_groups.borrow();
assert_eq!(groups.len(), 2);
// Verify that the first component group was parsed and has expected contents.
let first_group = &groups[0];
assert_eq!(first_group.name, "Test Group 1".to_string());
let color = first_group.color.unwrap();
assert_eq!((color.red * 255.0) as u8, 0xC0);
assert_eq!((color.green * 255.0) as u8, 0x47);
assert_eq!((color.blue * 255.0) as u8, 0xAB);
let expected_components =
vec!["Standard.Base.System.File.new".into(), "local.Unnamed_10.Main.main".into()];
assert_eq!(first_group.components, expected_components);
// Verify that the second component group was parsed and has expected contents.
assert_eq!(groups[1], ComponentGroup {
name: "Input".into(),
color: None,
components: vec!["Standard.Base.System.File.new".into(),],
});
});
}
}

View File

@ -810,7 +810,7 @@ mod test {
let context_data = execution_context::plain::test::MockData::new();
let Fixture { mut test, project, json_events_sender, .. } = Fixture::new(
|mock_json_client| {
ExecutionFixture::mock_create_push_destroy_calls(&context_data, mock_json_client);
ExecutionFixture::mock_default_calls(&context_data, mock_json_client);
mock_json_client.require_all_calls();
},
|_| {},

View File

@ -70,6 +70,18 @@ pub struct QualifiedName {
pub segments: Vec<QualifiedNameSegment>,
}
impl From<&str> for QualifiedName {
fn from(name: &str) -> Self {
name.split(ast::opr::predefined::ACCESS).collect()
}
}
impl From<String> for QualifiedName {
fn from(name: String) -> Self {
name.as_str().into()
}
}
impl From<QualifiedName> for String {
fn from(name: QualifiedName) -> Self {
String::from(&name)

View File

@ -1530,7 +1530,7 @@ The component group provided by a library.
```typescript
interface LibraryComponentGroup {
/**
* Thf fully qualified module name. A string consisting of a namespace and
* The fully qualified library name. A string consisting of a namespace and
* a library name separated by the dot <namespace>.<library name>,
* i.e. `Standard.Base`.
*/
@ -1538,9 +1538,9 @@ interface LibraryComponentGroup {
/** The group name without the library name prefix.
* E.g. given the `Standard.Base.Group 1` group reference,
* the `group` field contains `Group 1`.
* the `name` field contains `Group 1`.
*/
group: string;
name: string;
color?: string;

View File

@ -62,7 +62,7 @@ final class ComponentGroupsResolver {
.groupByKeepFirst(newLibraryComponentGroups) { libraryComponentGroup =>
GroupReference(
libraryComponentGroup.library,
libraryComponentGroup.group
libraryComponentGroup.name
)
}

View File

@ -76,14 +76,14 @@ object LibraryComponentGroups {
* the JSONRPC API.
*
* @param library the library name
* @param group the group name
* @param name the group name
* @param color the component group color
* @param icon the component group icon
* @param exports the list of components provided by this component group
*/
case class LibraryComponentGroup(
library: LibraryName,
group: GroupName,
name: GroupName,
color: Option[String],
icon: Option[String],
exports: Seq[LibraryComponent]
@ -103,7 +103,7 @@ object LibraryComponentGroup {
): LibraryComponentGroup =
LibraryComponentGroup(
library = libraryName,
group = componentGroup.group,
name = componentGroup.group,
color = componentGroup.color,
icon = componentGroup.icon,
exports = componentGroup.exports.map(LibraryComponent.fromComponent)
@ -120,7 +120,7 @@ object LibraryComponentGroup {
): LibraryComponentGroup =
LibraryComponentGroup(
library = extendedComponentGroup.group.libraryName,
group = extendedComponentGroup.group.groupName,
name = extendedComponentGroup.group.groupName,
color = None,
icon = None,
exports =
@ -130,7 +130,7 @@ object LibraryComponentGroup {
/** Fields for use when serializing the [[LibraryComponentGroup]]. */
private object Fields {
val Library = "library"
val Group = "group"
val Name = "name"
val Color = "color"
val Icon = "icon"
val Exports = "exports"
@ -145,7 +145,7 @@ object LibraryComponentGroup {
)
Json.obj(
(Fields.Library -> componentGroup.library.asJson) +:
(Fields.Group -> componentGroup.group.asJson) +:
(Fields.Name -> componentGroup.name.asJson) +:
(color.toSeq ++ icon.toSeq ++ exports.toSeq): _*
)
}
@ -154,11 +154,11 @@ object LibraryComponentGroup {
implicit val decoder: Decoder[LibraryComponentGroup] = { json =>
for {
library <- json.get[LibraryName](Fields.Library)
group <- json.get[GroupName](Fields.Group)
name <- json.get[GroupName](Fields.Name)
color <- json.get[Option[String]](Fields.Color)
icon <- json.get[Option[String]](Fields.Icon)
exports <- json.getOrElse[List[LibraryComponent]](Fields.Exports)(List())
} yield LibraryComponentGroup(library, group, color, icon, exports)
} yield LibraryComponentGroup(library, name, color, icon, exports)
}
}

View File

@ -301,14 +301,14 @@ object ComponentGroupsResolverSpec {
/** Create a new library component group. */
def libraryComponentGroup(
namespace: String,
name: String,
group: String,
libraryNamespace: String,
libraryName: String,
groupName: String,
exports: String*
): LibraryComponentGroup =
LibraryComponentGroup(
library = LibraryName(namespace, name),
group = GroupName(group),
library = LibraryName(libraryNamespace, libraryName),
name = GroupName(groupName),
color = None,
icon = None,
exports = exports.map(LibraryComponent(_, None))

View File

@ -921,7 +921,7 @@ class ContextRegistryTest extends BaseServerTest {
"componentGroups": [
{
"library" : "Standard.Base",
"group" : "Input",
"name" : "Input",
"exports" : [
{
"name" : "Standard.Base.File.new"

View File

@ -87,5 +87,4 @@ features = [
]
[dev-dependencies]
assert_approx_eq = { version = "1.1.0" }
wasm-bindgen-test = { version = "0.3.8" }

View File

@ -389,6 +389,51 @@ impl Rgb {
Self::new(r.into() / 255.0, g.into() / 255.0, b.into() / 255.0)
}
/// Return a color if the argument is a string matching one of the formats: `#RGB`, `#RRGGBB`,
/// `RGB`, or `RRGGBB`, where `R`, `G`, `B` represent lower- or upper-case hexadecimal digits.
/// The `RR`, `GG`, `BB` color components are mapped from `[00 - ff]` value range into `[0.0 -
/// 1.0]` (a three-digit string matching a `#RGB` or `RGB` format is equivalent to a six-digit
/// string matching a `#RRGGBB` format, constructed by duplicating the digits).
///
/// The format is based on the hexadecimal color notation used in CSS (see:
/// https://developer.mozilla.org/en-US/docs/Web/CSS/hex-color), with the following changes:
/// - the `#` character is optional,
/// - formats containing an alpha color component are not supported.
/// ```
/// # use ensogl_core::data::color::Rgb;
/// fn color_to_u8_tuple(c: Rgb) -> (u8, u8, u8) {
/// ((c.red * 255.0) as u8, (c.green * 255.0) as u8, (c.blue * 255.0) as u8)
/// }
///
/// assert_eq!(Rgb::from_css_hex("#C047AB").map(color_to_u8_tuple), Some((0xC0, 0x47, 0xAB)));
/// assert_eq!(Rgb::from_css_hex("#fff").map(color_to_u8_tuple), Some((0xff, 0xff, 0xff)));
/// assert_eq!(Rgb::from_css_hex("fff").map(color_to_u8_tuple), Some((0xff, 0xff, 0xff)));
/// assert_eq!(Rgb::from_css_hex("C047AB").map(color_to_u8_tuple), Some((0xC0, 0x47, 0xAB)));
/// assert!(Rgb::from_css_hex("red").is_none());
/// assert!(Rgb::from_css_hex("yellow").is_none());
/// assert!(Rgb::from_css_hex("#red").is_none());
/// assert!(Rgb::from_css_hex("#yellow").is_none());
/// assert!(Rgb::from_css_hex("#").is_none());
/// assert!(Rgb::from_css_hex("").is_none());
/// ```
pub fn from_css_hex(css_hex: &str) -> Option<Self> {
let hex_bytes = css_hex.strip_prefix('#').unwrap_or(css_hex).as_bytes();
let hex_color_components = match hex_bytes.len() {
3 => Some(Vector3([hex_bytes[0]; 2], [hex_bytes[1]; 2], [hex_bytes[2]; 2])),
6 => {
let (chunks, _) = hex_bytes.as_chunks::<2>();
Some(Vector3(chunks[0], chunks[1], chunks[2]))
}
_ => None,
};
hex_color_components.and_then(|components| {
let red = byte_from_hex(components.x)?;
let green = byte_from_hex(components.y)?;
let blue = byte_from_hex(components.z)?;
Some(Rgb::from_base_255(red, green, blue))
})
}
/// Converts the color to `LinearRgb` representation.
pub fn into_linear(self) -> LinearRgb {
self.into()
@ -403,6 +448,21 @@ impl Rgb {
}
}
// === Rgb Helpers ===
/// Decode an 8-bit number from its big-endian hexadecimal encoding in ASCII. Return `None` if any
/// of the bytes stored in the argument array is not an upper- or lower-case hexadecimal digit in
/// ASCII.
fn byte_from_hex(s: [u8; 2]) -> Option<u8> {
let first_digit = (s[0] as char).to_digit(16)? as u8;
let second_digit = (s[1] as char).to_digit(16)? as u8;
Some(first_digit << 4 | second_digit)
}
// === Rgba ===
impl Rgba {
/// Constructor.
pub fn black() -> Self {

View File

@ -821,8 +821,6 @@ mod tests {
use enso_web::TimeProvider;
use std::ops::AddAssign;
use assert_approx_eq::assert_approx_eq;
// === MockTimeProvider ===

View File

@ -20,6 +20,7 @@
#![feature(unboxed_closures)]
#![feature(trace_macros)]
#![feature(const_trait_impl)]
#![feature(slice_as_chunks)]
// === Standard Linter Configuration ===
#![deny(non_ascii_idents)]
#![warn(unsafe_code)]

View File

@ -201,8 +201,11 @@ macro_rules! make_rpc_methods {
impl Drop for Client {
fn drop(&mut self) {
if self.require_all_calls.get() && !std::thread::panicking() {
$(assert!(self.expect.$method.borrow().is_empty(),
"Didn't make expected call");)* //TODO[ao] print method name.
$(
let method = stringify!($method);
let msg = iformat!("An expected call to {method} was not made.");
assert!(self.expect.$method.borrow().is_empty(), "{}", msg);
)*
}
}
}

View File

@ -18,6 +18,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
enso-shapely = { version = "^0.2.0", path = "../shapely" }
anyhow = "1.0.37"
assert_approx_eq = { version = "1.1.0" }
backtrace = "0.3.53"
boolinator = "2.4.0"
cfg-if = "1.0.0"

View File

@ -68,6 +68,7 @@ pub use tp::*;
pub use vec::*;
pub use wrapper::*;
pub use assert_approx_eq::assert_approx_eq;
pub use boolinator::Boolinator;
pub use derivative::Derivative;
pub use derive_more::*;

View File

@ -10,6 +10,3 @@ edition = "2021"
nalgebra = { version = "0.26.1" }
num-traits = { version = "0.2" }
paste = "1.0.7"
[dev-dependencies]
assert_approx_eq = { version = "1.1.0" }