Filtering of Virtual Entries in Component Browser by input & return types. (#3652)

Filter the Virtual Entries displayed in the Component Browser based on the input type and the return type of the edited expression. Only the Virtual Entries with matching types are displayed.

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

#### Visuals

See below for two screenshots, showing the Component Browser with an empty expression and with the input port not connected. Two virtual entries are visible, with documentation.

<img width="871" alt="Screenshot 2022-08-16 at 15 19 55" src="https://user-images.githubusercontent.com/273837/184894042-0f64986d-2c89-4c94-90cf-215467e297c7.png">

<img width="866" alt="Screenshot 2022-08-16 at 15 20 04" src="https://user-images.githubusercontent.com/273837/184894059-5089414c-c1f3-407c-a6da-38fd9c3e7c81.png">

See below for a screenshot showing the Component Browser with `5.div ` expression entered. The `div` method expects an `Integer` argument. Only the `number input` virtual component is visible.

<img width="504" alt="Screenshot 2022-08-16 at 15 23 07" src="https://user-images.githubusercontent.com/273837/184894081-358ae780-cfca-4002-b7da-4be57f0357c1.png">

See below for a screenshot showing the Component Browser with `Geo.point ` expression entered. The `point` method expects a `Decimal` argument. Only the `number input` virtual component is visible.

<img width="500" alt="Screenshot 2022-08-16 at 15 24 37" src="https://user-images.githubusercontent.com/273837/184894119-d4f5e7e1-5948-4d8f-adc4-f94a2f4c7245.png">

See below for a screenshot showing the Component Browser with `5.format ` expression entered. The `format` method expects a `Text` argument. Only the `text input` virtual component is visible.


<img width="480" alt="Screenshot 2022-08-16 at 15 55 27" src="https://user-images.githubusercontent.com/273837/184897598-465569c8-8319-43ac-9589-4384ffdc33cc.png">

See below for a video showing that the Component Browser opened with the input port connected to a typed node. The Component Browser does not show virtual entries.

https://user-images.githubusercontent.com/273837/184914619-957a7369-1019-4636-989d-96d65954642f.mov

# Important Notes
- Unused code from the old `enso_gui::controller::searcher::action::hardcoded` module is removed. The `enso_gui::controller::searcher::component::hardcoded::Snippet` type is redefined to contain only the used fields from the old `action::hardcoded::Suggestion` type.
This commit is contained in:
Mateusz Czapliński 2022-08-18 11:40:41 +02:00 committed by GitHub
parent 3c12a8c0a2
commit fc089857d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 108 additions and 361 deletions

View File

@ -976,14 +976,16 @@ impl Searcher {
let mut data = this.data.borrow_mut();
data.actions = Actions::Loaded { list: Rc::new(list) };
let completions = responses.iter().flat_map(|r| r.results.iter().cloned());
data.components = this.make_component_list(completions);
data.components =
this.make_component_list(completions, &this_type, &return_types);
}
Err(err) => {
let msg = "Request for completions to the Language Server returned error";
error!(this.logger, "{msg}: {err}");
let mut data = this.data.borrow_mut();
data.actions = Actions::Error(Rc::new(err.into()));
data.components = this.make_component_list(this.database.keys());
data.components =
this.make_component_list(this.database.keys(), &this_type, &return_types);
}
}
this.notifier.publish(Notification::NewActionList).await;
@ -1001,10 +1003,6 @@ impl Searcher {
let mut actions = action::ListWithSearchResultBuilder::new();
let (libraries_icon, default_icon) =
action::hardcoded::ICONS.with(|i| (i.libraries.clone_ref(), i.default.clone_ref()));
//TODO[ao] should be uncommented once new searcher GUI will be integrated + the order of
// added entries should be adjusted.
// https://github.com/enso-org/ide/issues/1681
// Self::add_hardcoded_entries(&mut actions,this_type,return_types)?;
if should_add_additional_entries && self.ide.manage_projects().is_ok() {
let mut root_cat = actions.add_root_category("Projects", default_icon.clone_ref());
let category = root_cat.add_category("Projects", default_icon.clone_ref());
@ -1046,8 +1044,11 @@ impl Searcher {
fn make_component_list<'a>(
&self,
entry_ids: impl IntoIterator<Item = suggestion_database::entry::Id>,
this_type: &Option<String>,
return_types: &[String],
) -> component::List {
let mut builder = self.list_builder_with_favorites.deref().clone();
add_virtual_entries_to_builder(&mut builder, this_type, return_types);
builder.extend_list_and_allow_favorites_with_ids(&self.database, entry_ids);
builder.build()
}
@ -1162,23 +1163,6 @@ impl Searcher {
libraries_cat_builder.add_action(action);
}
}
//TODO[ao] The usage of add_hardcoded_entries_to_list is currently commented out. It should be
// uncommented when working on https://github.com/enso-org/ide/issues/1681.
#[allow(dead_code)]
fn add_hardcoded_entries(
list: &mut action::ListBuilder,
this_type: Option<String>,
return_types: Vec<String>,
) -> FallibleResult {
let this_type = this_type.map(tp::QualifiedName::from_text).transpose()?;
let rt_converted = return_types.iter().map(tp::QualifiedName::from_text);
let rt_result: FallibleResult<HashSet<tp::QualifiedName>> = rt_converted.collect();
let return_types = rt_result?;
let return_types = if return_types.is_empty() { None } else { Some(&return_types) };
action::hardcoded::add_hardcoded_entries_to_list(list, this_type.as_ref(), return_types);
Ok(())
}
}
@ -1194,13 +1178,28 @@ fn component_list_builder_with_favorites<'a>(
builder = builder.with_local_scope_module_id(id);
}
builder.set_grouping_and_order_of_favorites(suggestion_db, groups);
let base_lib_qn = project::QualifiedName::standard_base_library();
let input_group_name = component::hardcoded::INPUT_GROUP_NAME;
let snippets = component::hardcoded::INPUT_SNIPPETS.with(|s| s.clone());
builder.insert_virtual_components_in_favorites_group(input_group_name, base_lib_qn, snippets);
builder
}
fn add_virtual_entries_to_builder(
builder: &mut component::builder::List,
this_type: &Option<String>,
return_types: &[String],
) {
if this_type.is_none() {
let snippets = if return_types.is_empty() {
component::hardcoded::INPUT_SNIPPETS.with(|s| s.clone())
} else {
let parse_type_qn = |s| tp::QualifiedName::from_text(s).ok();
let rt_qns = return_types.iter().filter_map(parse_type_qn);
component::hardcoded::input_snippets_with_matching_return_type(rt_qns)
};
let group_name = component::hardcoded::INPUT_GROUP_NAME;
let project = project::QualifiedName::standard_base_library();
builder.insert_virtual_components_in_favorites_group(group_name, project, snippets);
}
}
// === Node Edit Metadata Guard ===

View File

@ -26,7 +26,7 @@ pub enum Suggestion {
/// The suggestion from Suggestion Database received from the Engine.
FromDatabase(Rc<model::suggestion_database::Entry>),
/// The one of the hard-coded suggestion.
Hardcoded(Rc<hardcoded::Suggestion>),
Hardcoded(Rc<controller::searcher::component::hardcoded::Snippet>),
}
impl Suggestion {
@ -38,10 +38,8 @@ impl Suggestion {
) -> CodeToInsert {
match self {
Suggestion::FromDatabase(s) => s.code_to_insert(current_module, generate_this),
Suggestion::Hardcoded(s) => CodeToInsert {
code: s.code.to_owned(),
imports: s.imports.iter().cloned().collect(),
},
Suggestion::Hardcoded(s) =>
CodeToInsert { code: s.code.to_owned(), imports: default() },
}
}
@ -50,8 +48,7 @@ impl Suggestion {
match self {
Suggestion::FromDatabase(suggestion) =>
suggestion.arguments.iter().map(|a| a.repr_type.clone()).collect(),
Suggestion::Hardcoded(suggestion) =>
suggestion.argument_types.iter().map(|t| t.into()).collect(),
Suggestion::Hardcoded(_) => vec![],
}
}
@ -69,7 +66,7 @@ impl Suggestion {
pub fn method_id(&self) -> Option<MethodId> {
match self {
Suggestion::FromDatabase(s) => s.method_id(),
Suggestion::Hardcoded(s) => s.method_id.clone(),
Suggestion::Hardcoded(_) => None,
}
}
}
@ -100,17 +97,6 @@ pub enum Action {
// In the future, other action types will be added (like module/method management, etc.).
}
impl Action {
/// Get the name of the icon associated with given action.
pub fn icon(&self) -> ImString {
use Suggestion::*;
match self {
Self::Suggestion(Hardcoded(s)) => s.icon.clone_ref(),
_ => hardcoded::ICONS.with(|ics| ics.default.clone_ref()),
}
}
}
impl Display for Action {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {

View File

@ -1,15 +1,7 @@
//! A module containing the hard-coded definitions displayed in Searcher. The main function to use
//! is [`add_hardcoded_entries_to_list`] which adds the entries to given [`ListBuilder`].
//! A module containing the hard-coded names of [`Icons`] displayed in Searcher.
use crate::prelude::*;
use crate::controller::searcher::action;
use crate::controller::searcher::action::ListBuilder;
use crate::model::module::MethodId;
use double_representation::module;
use double_representation::tp;
// =============
@ -21,12 +13,6 @@ use double_representation::tp;
#[derive(Clone, CloneRef, Debug, Default)]
pub struct Icons {
pub search_result: ImString,
pub data_science: ImString,
pub input_output: ImString,
pub text: ImString,
pub number_input: ImString,
pub text_input: ImString,
pub data_input: ImString,
pub libraries: ImString,
pub default: ImString,
}
@ -35,271 +21,7 @@ thread_local! {
/// A set of hardcoded icon names, to be used when creating hardcoded categories and actions.
pub static ICONS:Icons = Icons {
search_result : ImString::new("search_result"),
data_science : ImString::new("data_science"),
input_output : ImString::new("io"),
text : ImString::new("text"),
number_input : ImString::new("number_input"),
text_input : ImString::new("text_input"),
data_input : ImString::new("data_input"),
libraries : ImString::new("libraries"),
default : ImString::new("default"),
};
}
// ===================
// === Definitions ===
// ===================
// === RootCategory ===
/// The hardcoded root category.
///
/// The structure is used solely for defining hierarchy of hard-coded suggestions. Based in this
/// hierarchy, the [`add_hardcoded_entries_to_list`] will add analogous [`action::RootCategory`]
/// to the built list.
#[allow(missing_docs)]
#[derive(Clone, Debug)]
pub struct RootCategory {
pub name: &'static str,
pub icon: ImString,
pub categories: Vec<Subcategory>,
}
// === Category ===
/// The hardcoded second-tier category.
///
/// The structure is used solely for defining hierarchy of hard-coded suggestions. Based in this
/// hierarchy, the [`add_hardcoded_entries_to_list`] will add analogous [`action::Category`]
/// to the built list.
#[allow(missing_docs)]
#[derive(Clone, Debug)]
pub struct Subcategory {
pub name: &'static str,
pub icon: ImString,
pub suggestions: Vec<Rc<Suggestion>>,
}
// === Suggestion ===
/// The hardcoded suggestion.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Suggestion {
/// The name displayed in the Searcher.
pub name: &'static str,
/// The code inserted when picking suggestion.
pub code: &'static str,
/// The type of expected `self` argument.
pub this_arg: Option<tp::QualifiedName>,
/// The list of argument types which may be applied to the code returned by this suggestion.
pub argument_types: Vec<tp::QualifiedName>,
/// The type returned by the suggestion's code.
pub return_type: Option<tp::QualifiedName>,
/// An import required by the suggestion.
pub imports: Vec<module::QualifiedName>,
/// The documentation bound to the suggestion.
pub documentation_html: Option<String>,
/// The id of the method called by the suggestion.
pub method_id: Option<MethodId>,
/// The name of the icon bound to this entry.
pub icon: ImString,
}
impl Suggestion {
/// Construct a hardcoded suggestion with given name, code, and icon.
pub(crate) fn new(name: &'static str, code: &'static str, icon: &ImString) -> Self {
let icon = icon.clone_ref();
Self { name, code, icon, ..default() }
}
fn with_this_arg(mut self, this_arg: impl TryInto<tp::QualifiedName, Error: Debug>) -> Self {
self.this_arg = Some(this_arg.try_into().unwrap());
self
}
fn with_argument_types<Iter>(mut self, argument_types: Iter) -> Self
where
Iter: IntoIterator,
Iter::Item: TryInto<tp::QualifiedName, Error: Debug>, {
let conv_results = argument_types.into_iter().map(|arg| arg.try_into());
let result = conv_results.collect::<Result<Vec<tp::QualifiedName>, _>>();
self.argument_types = result.unwrap();
self
}
/// Returns a modified suggestion with [`Suggestion::return_type`] field set. This method is
/// only intended to be used when defining hardcoded suggestions and panics if the argument
/// fails to convert to a valid type name.
pub(crate) fn with_return_type(
mut self,
return_type: impl TryInto<tp::QualifiedName, Error: Debug>,
) -> Self {
self.return_type = Some(return_type.try_into().unwrap());
self
}
fn with_import_added(
mut self,
import: impl TryInto<module::QualifiedName, Error: Debug>,
) -> Self {
self.imports.push(import.try_into().unwrap());
self
}
/// Returns a modified suggestion with [`Suggestion::documentation_html`] field set. This
/// method is only intended to be used when defining hardcoded suggestions and panics if a
/// documentation parser cannot be created or the argument fails to parse as valid
/// documentation.
pub(crate) fn with_documentation(mut self, documentation: &str) -> Self {
let doc_parser = parser::DocParser::new().unwrap();
let doc_string = documentation.to_string();
let documentation_html = doc_parser.generate_html_doc_pure(doc_string);
self.documentation_html = Some(documentation_html.unwrap());
self
}
fn marked_as_method_call(
mut self,
name: &'static str,
module: impl TryInto<module::QualifiedName, Error: Debug>,
) -> Self {
self.method_id = Some(MethodId {
module: module.try_into().unwrap(),
defined_on_type: self.this_arg.as_ref().unwrap().clone(),
name: name.to_owned(),
});
self
}
fn marked_as_module_method_call(
mut self,
name: &'static str,
module: impl TryInto<module::QualifiedName, Error: Debug>,
) -> Self {
let module = module.try_into().unwrap();
self.method_id = Some(MethodId {
module: module.clone(),
defined_on_type: module.into(),
name: name.to_owned(),
});
self
}
}
// ======================================
// === The Hardcoded Suggestions List ===
// ======================================
// The constant must be thread local because of using Rc inside. It should not affect the
// application much, because we are in a single thread anyway.
thread_local! {
/// The suggestions constant.
pub static SUGGESTIONS:Vec<RootCategory> = ICONS.with(|icons| vec![
RootCategory {
name : "Data Science",
icon : icons.data_science.clone_ref(),
categories : vec![
Subcategory {
name : "Input / Output",
icon : icons.input_output.clone_ref(),
suggestions : vec![
Rc::new(
Suggestion::new("Text Input","\"\"",&icons.text_input)
.with_return_type("Standard.Base.Data.Text.Text")
),
Rc::new(
Suggestion::new("Number Input","0",&icons.number_input)
.with_return_type("Standard.Base.Data.Numbers.Number")
),
]
},
Subcategory {
name : "Text",
icon : icons.text.clone_ref(),
suggestions : vec![
Rc::new(
Suggestion::new("Text Length","length",&icons.default)
.with_this_arg("Standard.Base.Data.Text.Text")
.with_return_type("Standard.Base.Data.Numbers.Integer")
.marked_as_method_call("length","Standard.Base.Data.Text.Extensions")
)
]
}
]
},
RootCategory {
name : "Network",
icon : icons.default.clone_ref(),
categories : vec![
Subcategory {
name : "HTTP",
icon : icons.default.clone_ref(),
suggestions : vec![
Rc::new(
Suggestion::new("Fetch Data", "Http.fetch",&icons.default)
.with_return_type("Standard.Base.Network.Http.Body.Body")
.with_argument_types(vec![
"Standard.Base.Data.Text.Text",
"Vector.Vector",
])
.with_import_added("Standard.Base.Network.Http")
.marked_as_module_method_call("fetch","Standard.Base.Network.Http")
),
Rc::new(
Suggestion::new("GET Request", "Http.get",&icons.default)
.with_return_type("Standard.Base.Network.Http.Response.Response")
.with_import_added("Standard.Base.Network.Http")
.marked_as_module_method_call("get","Standard.Base.Network.Http")
)
]
}
]
}
]);
}
/// Extend the list built by given [`ListBuilder`] with the categories and actions hardcoded
/// in [`SUGGESTIONS`] constant.
pub fn add_hardcoded_entries_to_list(
list: &mut ListBuilder,
this_type: Option<&tp::QualifiedName>,
return_types: Option<&HashSet<tp::QualifiedName>>,
) {
SUGGESTIONS.with(|hardcoded| {
for hc_root_category in hardcoded {
let icon = hc_root_category.icon.clone_ref();
let mut root_cat = list.add_root_category(hc_root_category.name, icon);
for hc_category in &hc_root_category.categories {
let icon = hc_root_category.icon.clone_ref();
let category = root_cat.add_category(hc_category.name, icon);
category.extend(hc_category.suggestions.iter().cloned().filter_map(|suggestion| {
let this_type_matches = if let Some(this_type) = this_type {
suggestion.this_arg.contains(this_type)
} else {
true
};
let return_type_matches = if let Some(return_types) = return_types {
suggestion
.return_type
.as_ref()
.map_or(false, |rt| return_types.contains(rt))
} else {
true
};
let filtered_in = this_type_matches && return_type_matches;
filtered_in.as_some_from(|| {
action::Action::Suggestion(action::Suggestion::Hardcoded(suggestion))
})
}));
}
}
});
}

View File

@ -7,19 +7,11 @@
use crate::prelude::*;
use double_representation::tp;
use ide_view_component_group::icon::Id as IconId;
// ====================
// === Type Aliases ===
// ====================
/// A hardcoded snippet of code with a description and syntactic metadata.
pub type Snippet = controller::searcher::action::hardcoded::Suggestion;
// =================
// === Constants ===
// =================
@ -34,30 +26,90 @@ thread_local! {
/// displayed in the Component Browser as virtual components they allow the users to easily
/// enter primitive literals in code.
pub static INPUT_SNIPPETS: Vec<Rc<Snippet>> = vec![
snippet_with_name_and_code_and_icon("text input", "\"\"", IconId::TextInput)
.with_return_type("Standard.Base.Data.Text.Text")
Snippet::new("text input", "\"\"", IconId::TextInput)
.with_return_types(["Standard.Base.Data.Text.Text"])
.with_documentation(
"A text input node.\n\n\
An empty text. The value can be edited and used as an input for other nodes."
An empty text. The value can be edited and used as an input for other nodes.",
)
.into(),
snippet_with_name_and_code_and_icon("number input", "0", IconId::NumberInput)
.with_return_type("Standard.Base.Data.Numbers.Number")
Snippet::new("number input", "0", IconId::NumberInput)
.with_return_types([
"Standard.Base.Data.Numbers.Number",
"Standard.Base.Data.Numbers.Decimal",
"Standard.Base.Data.Numbers.Integer",
])
.with_documentation(
"A number input node.\n\n\
A zero number. The value can be edited and used as an input for other nodes."
"A number input node.\n\n\
A zero number. The value can be edited and used as an input for other nodes.",
)
.into(),
];
}
// === Constants helpers ===
// === Filtering by Return Type ===
fn snippet_with_name_and_code_and_icon(
name: &'static str,
code: &'static str,
icon: IconId,
) -> Snippet {
Snippet::new(name, code, &ImString::new(icon.as_str()))
/// Return a filtered copy of [`INPUT_SNIPPETS`] containing only snippets which have at least one
/// of their return types on the given list of return types.
pub fn input_snippets_with_matching_return_type(
return_types: impl IntoIterator<Item = tp::QualifiedName>,
) -> Vec<Rc<Snippet>> {
let rt_set: HashSet<_> = return_types.into_iter().collect();
let rt_of_snippet_is_in_set =
|s: &&Rc<Snippet>| s.return_types.iter().any(|rt| rt_set.contains(rt));
INPUT_SNIPPETS
.with(|snippets| snippets.iter().filter(rt_of_snippet_is_in_set).cloned().collect_vec())
}
// ===============
// === Snippet ===
// ===============
/// A hardcoded snippet of code with a description and syntactic metadata.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Snippet {
/// The name displayed in the [Component Browser](crate::controller::searcher).
pub name: &'static str,
/// The code inserted when picking the snippet.
pub code: &'static str,
/// A list of types that the return value of this snippet's code typechecks as. Used by the
/// [Component Browser](crate::controller::searcher) to decide whether to display the
/// snippet when filtering components by return type.
pub return_types: Vec<tp::QualifiedName>,
/// The documentation bound to the snippet.
pub documentation_html: Option<String>,
/// The ID of the icon bound to this snippet's entry in the [Component
/// Browser](crate::controller::searcher).
pub icon: IconId,
}
impl Snippet {
/// Construct a hardcoded snippet with given name, code, and icon.
fn new(name: &'static str, code: &'static str, icon: IconId) -> Self {
Self { name, code, icon, ..default() }
}
/// Returns a modified suggestion with [`Snippet::return_types`] field set. This method is only
/// intended to be used when defining hardcoded suggestions and panics if any of the given
/// return types fail to convert to a valid type name.
fn with_return_types<'a>(mut self, return_types: impl IntoIterator<Item = &'a str>) -> Self {
let types = return_types.into_iter().map(|rt| rt.try_into().unwrap()).collect_vec();
self.return_types = types;
self
}
/// Returns a modified suggestion with [`Snippet::documentation_html`] field set. This method
/// is only intended to be used when defining hardcoded suggestions and panics if a
/// documentation parser cannot be created or the argument fails to parse as valid
/// documentation.
fn with_documentation(mut self, documentation: &str) -> Self {
let doc_parser = parser::DocParser::new().unwrap();
let doc_string = documentation.to_string();
let documentation_html = doc_parser.generate_html_doc_pure(doc_string);
self.documentation_html = Some(documentation_html.unwrap());
self
}
}

View File

@ -193,19 +193,7 @@ impl list_view::entry::ModelProvider<component_group_view::Entry> for Component
let icon = icon_name.and_then(|n| n.to_pascal_case().parse().ok());
icon.unwrap_or_else(|| for_each_kind_variant!(kind_to_icon(kind)))
}
component::Data::Virtual { snippet } => {
let icon = &snippet.icon;
let parsed_icon = component_group_view::icon::Id::from_str(icon);
parsed_icon.unwrap_or_else(|_| {
let msg = iformat!(
"A virtual component named " snippet.name;?
" uses an icon name " icon
" which is not found among predefined icons. A default icon will be used."
);
event!(ERROR, "{msg}");
default()
})
}
component::Data::Virtual { snippet } => snippet.icon,
};
Some(component_group_view::entry::Model {
icon,