mirror of
https://github.com/enso-org/enso.git
synced 2024-09-11 13:15:52 +03:00
Breadcrumbs integration (#3720)
[ci no changelog needed] [Task link](https://www.pivotaltracker.com/story/show/182675703) This PR implements the actual integration of the breadcrumbs panel with the component list panel. A special breadcrumbs controller (`controller::searcher::breadcrumbs`) is tracking the currently opened module with a list of its parents. The searcher presenter uses the API of the controller to sync the displayed list of breadcrumbs with the controller state. https://user-images.githubusercontent.com/6566674/193064122-7d3fc4d6-9148-4ded-a73e-767ac9ac83f8.mp4 # Important Notes - There is an `All` breadcrumb displayed at all times at the beginning of the list. It will be replaced with a section name as part of [Section Title on Component Browser's Breadcrumbs Panel](https://www.pivotaltracker.com/story/show/182610561) task. - I changed the implementation of `project::main_module_id`, `project::QualifiedName::main_module`, and `API::main_module` so that they are logically connected which each other. - I adjusted the Breadcrumbs View to avoid "appearance" animation glitches when opening new modules. `set_entries` was replaced with the `set_entries_from` endpoint.
This commit is contained in:
parent
11acad5cff
commit
0d74ab6124
@ -91,8 +91,6 @@ impl Display for Id {
|
||||
|
||||
impl Id {
|
||||
/// Construct a module's ID value from a name segments sequence.
|
||||
///
|
||||
/// Fails if the given sequence is empty.
|
||||
pub fn new(segments: impl IntoIterator<Item = ReferentName>) -> Id {
|
||||
let segments = segments.into_iter().collect_vec();
|
||||
Id { segments }
|
||||
@ -100,7 +98,7 @@ impl Id {
|
||||
|
||||
/// Construct a module's ID value from a name segments sequence.
|
||||
///
|
||||
/// Fails if the sequence is empty or if any of the segments is not a valid referent name.
|
||||
/// Fails if any of the segments is not a valid referent name.
|
||||
pub fn try_new(segments: impl IntoIterator<Item: AsRef<str>>) -> FallibleResult<Id> {
|
||||
let texts = segments.into_iter();
|
||||
let names = texts.map(|text| ReferentName::new(text.as_ref()));
|
||||
@ -175,11 +173,8 @@ impl QualifiedName {
|
||||
}
|
||||
|
||||
/// Create a qualified name for the project's main module.
|
||||
///
|
||||
/// It is special, as its name consists only from the project name, unlike other modules'
|
||||
/// qualified names.
|
||||
pub fn new_main(project_name: project::QualifiedName) -> QualifiedName {
|
||||
Self::new(project_name, Id::new(std::iter::empty()))
|
||||
Self::new(project_name, project::main_module_id())
|
||||
}
|
||||
|
||||
/// Constructs a qualified name from its text representation.
|
||||
@ -295,7 +290,16 @@ impl QualifiedName {
|
||||
|
||||
/// Check if the name refers to some library's top module.
|
||||
pub fn is_top_module(&self) -> bool {
|
||||
self.id.segments.len() == 1
|
||||
self.id.segments.len() <= 1
|
||||
}
|
||||
|
||||
/// Check if the name refers to some project's Main module.
|
||||
pub fn is_main_module(&self) -> bool {
|
||||
match self.id.segments.len() {
|
||||
0 => true,
|
||||
1 if self.id.segments[0] == PROJECTS_MAIN_MODULE => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the top module containing the module referred by this name. Return self if it is already
|
||||
@ -307,9 +311,24 @@ impl QualifiedName {
|
||||
/// Get the parent module of the module referred by this name. Returns [`None`] if it is a top
|
||||
/// module.
|
||||
pub fn parent_module(&self) -> Option<Self> {
|
||||
let id = Id::try_new(self.id.parent_segments()).ok()?;
|
||||
let project_name = self.project_name.clone();
|
||||
Some(Self { project_name, id })
|
||||
if self.is_top_module() {
|
||||
None
|
||||
} else {
|
||||
let id = Id::try_new(self.id.parent_segments()).ok()?;
|
||||
let project_name = self.project_name.clone();
|
||||
Some(Self { project_name, id })
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an iterator over all parent modules. The `self` is not included.
|
||||
pub fn parent_modules(&self) -> impl Iterator<Item = Self> {
|
||||
let mut current = self.clone();
|
||||
iter::from_fn(move || {
|
||||
current.parent_module().map(|parent| {
|
||||
current = parent.clone();
|
||||
parent
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,11 @@ pub const BASE_LIBRARY_NAME: &str = "Base";
|
||||
/// The full path of the [`BASE_LIBRARY_NAME`] project in the [`STANDARD_NAMESPACE`].
|
||||
pub const STANDARD_BASE_LIBRARY_PATH: &str = concatcp!(STANDARD_NAMESPACE, ".", BASE_LIBRARY_NAME);
|
||||
|
||||
|
||||
/// The identifier of the project's main module.
|
||||
pub fn main_module_id() -> crate::module::Id {
|
||||
// We can just assume that `PROJECTS_MAIN_MODULE` is valid. This is verified by a test.
|
||||
crate::module::Id::try_new([ast::constants::PROJECTS_MAIN_MODULE]).unwrap()
|
||||
}
|
||||
|
||||
// ==============
|
||||
// === Errors ===
|
||||
@ -179,4 +183,10 @@ mod test {
|
||||
fn qualified_name_of_standard_base_library_does_not_panic() {
|
||||
let _ = QualifiedName::standard_base_library();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn main_module_id_test() {
|
||||
// Should not panic.
|
||||
main_module_id();
|
||||
}
|
||||
}
|
||||
|
@ -22,12 +22,6 @@ use parser::Parser;
|
||||
/// The label of compiling stdlib message process.
|
||||
pub const COMPILING_STDLIB_LABEL: &str = "Compiling standard library. It can take up to 1 minute.";
|
||||
|
||||
/// The name of the module initially opened in the project view.
|
||||
///
|
||||
/// Currently, this name is hardcoded in the engine services and is populated for each project
|
||||
/// created using engine's Project Picker service.
|
||||
pub const INITIAL_MODULE_NAME: &str = "Main";
|
||||
|
||||
/// Name of the main definition.
|
||||
///
|
||||
/// This is the definition whose graph will be opened on IDE start.
|
||||
@ -52,12 +46,6 @@ pub fn main_method_ptr(
|
||||
module_path.method_pointer(project_name, MAIN_DEFINITION_NAME)
|
||||
}
|
||||
|
||||
/// The identifier of the project's main module.
|
||||
pub fn main_module_id() -> model::module::Id {
|
||||
// We can just assume that `INITIAL_MODULE_NAME` is valid. This is verified by a test.
|
||||
model::module::Id::try_new([INITIAL_MODULE_NAME]).unwrap()
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =================
|
||||
@ -124,7 +112,7 @@ impl Project {
|
||||
pub async fn initialize(&self) -> FallibleResult<InitializationResult> {
|
||||
let project = self.model.clone_ref();
|
||||
let parser = self.model.parser();
|
||||
let module_path = self.initial_module_path()?;
|
||||
let module_path = self.initial_module_path();
|
||||
let file_path = module_path.file_path().clone();
|
||||
|
||||
// TODO [mwu] This solution to recreate missing main file should be considered provisional
|
||||
@ -161,7 +149,7 @@ impl Project {
|
||||
|
||||
impl Project {
|
||||
/// Returns the path to the initially opened module in the given project.
|
||||
fn initial_module_path(&self) -> FallibleResult<model::module::Path> {
|
||||
fn initial_module_path(&self) -> model::module::Path {
|
||||
crate::ide::initial_module_path(&self.model)
|
||||
}
|
||||
|
||||
@ -262,12 +250,6 @@ mod tests {
|
||||
enso_config::engine_version_requirement();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn main_module_id_test() {
|
||||
// Should not panic.
|
||||
main_module_id();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_project_engine_version_fills_requirements() {
|
||||
let requirements = enso_config::engine_version_requirement();
|
||||
|
@ -616,7 +616,7 @@ impl Searcher {
|
||||
/// The list of modules and their content displayed in `Submodules` section of the browser.
|
||||
pub fn top_modules(&self) -> group::AlphabeticalList {
|
||||
let components = self.components();
|
||||
if let Some(selected) = self.breadcrumbs.currently_selected() {
|
||||
if let Some(selected) = self.breadcrumbs.selected() {
|
||||
components.submodules_of(selected).map(CloneRef::clone_ref).unwrap_or_default()
|
||||
} else {
|
||||
components.top_modules().clone_ref()
|
||||
@ -637,7 +637,7 @@ impl Searcher {
|
||||
/// The list of components displayed in `Local Scope` section of the browser.
|
||||
pub fn local_scope(&self) -> group::Group {
|
||||
let components = self.components();
|
||||
if let Some(selected) = self.breadcrumbs.currently_selected() {
|
||||
if let Some(selected) = self.breadcrumbs.selected() {
|
||||
components.get_module_content(selected).map(CloneRef::clone_ref).unwrap_or_default()
|
||||
} else {
|
||||
components.local_scope
|
||||
@ -646,7 +646,31 @@ impl Searcher {
|
||||
|
||||
/// Enter the specified module. The displayed content of the browser will be updated.
|
||||
pub fn enter_module(&self, module: &component::Id) {
|
||||
self.breadcrumbs.push(*module);
|
||||
let builder = breadcrumbs::Builder::new(&self.database, self.components());
|
||||
let breadcrumbs = builder.build(module);
|
||||
self.breadcrumbs.set_content(breadcrumbs);
|
||||
self.notifier.notify(Notification::NewActionList);
|
||||
}
|
||||
|
||||
/// Whether the last module in the breadcrumbs list contains more descendants or not.
|
||||
pub fn last_module_has_submodules(&self) -> bool {
|
||||
let last_module = self.breadcrumbs.last();
|
||||
let components = self.components();
|
||||
let get_submodules = |module| components.submodules_of(module).map(CloneRef::clone_ref);
|
||||
let submodules = last_module.and_then(get_submodules);
|
||||
submodules.map_or(false, |submodules| !submodules.is_empty())
|
||||
}
|
||||
|
||||
/// A list of breadcrumbs' text labels to be displayed. The list is updated by
|
||||
/// [`Self::enter_module`].
|
||||
pub fn breadcrumbs(&self) -> Vec<ImString> {
|
||||
self.breadcrumbs.names()
|
||||
}
|
||||
|
||||
/// Select the breadcrumb with the index [`id`]. The displayed content of the browser will be
|
||||
/// updated.
|
||||
pub fn select_breadcrumb(&self, id: usize) {
|
||||
self.breadcrumbs.select(id);
|
||||
self.notifier.notify(Notification::NewActionList);
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,10 @@ use crate::prelude::*;
|
||||
|
||||
use crate::controller::searcher::component;
|
||||
|
||||
use double_representation::module;
|
||||
use model::suggestion_database::entry::QualifiedName;
|
||||
use model::suggestion_database::Entry;
|
||||
|
||||
|
||||
|
||||
// ===================
|
||||
@ -11,13 +15,12 @@ use crate::controller::searcher::component;
|
||||
// ===================
|
||||
|
||||
/// A controller that keeps the path of entered modules in the Searcher and provides the
|
||||
/// functionality of the breadcrumbs panel.
|
||||
///
|
||||
/// TODO: The actual implementation would be finished in
|
||||
/// [Breadcrumbs Panel integration task](https://www.pivotaltracker.com/story/show/182675703).
|
||||
/// functionality of the breadcrumbs panel. The integration between the
|
||||
/// controller and the view is done by the [searcher presenter](crate::presenter::searcher).
|
||||
#[derive(Debug, Clone, CloneRef, Default)]
|
||||
pub struct Breadcrumbs {
|
||||
currently_selected: Rc<Cell<Option<component::Id>>>,
|
||||
list: Rc<RefCell<Vec<BreadcrumbEntry>>>,
|
||||
selected: Rc<Cell<usize>>,
|
||||
}
|
||||
|
||||
impl Breadcrumbs {
|
||||
@ -26,18 +29,151 @@ impl Breadcrumbs {
|
||||
default()
|
||||
}
|
||||
|
||||
/// Push the new breadcrumb to the breadcrumbs panel.
|
||||
pub fn push(&self, id: component::Id) {
|
||||
self.currently_selected.set(Some(id));
|
||||
/// Set the list of breadcrumbs to be displayed in the breadcrumbs panel.
|
||||
pub fn set_content(&self, breadcrumbs: impl Iterator<Item = BreadcrumbEntry>) {
|
||||
let mut borrowed = self.list.borrow_mut();
|
||||
*borrowed = breadcrumbs.collect();
|
||||
self.select(borrowed.len());
|
||||
}
|
||||
|
||||
/// Returns true if the currently selected breadcrumb is the root one.
|
||||
/// A list of breadcrumbs' text labels to be displayed in the panel.
|
||||
pub fn names(&self) -> Vec<ImString> {
|
||||
self.list.borrow().iter().map(|entry| entry.name()).collect()
|
||||
}
|
||||
|
||||
/// The last (right-most) breadcrumb in the list.
|
||||
pub fn last(&self) -> Option<component::Id> {
|
||||
self.list.borrow().last().map(BreadcrumbEntry::id)
|
||||
}
|
||||
|
||||
/// Mark the entry with the given index as selected.
|
||||
pub fn select(&self, id: usize) {
|
||||
self.selected.set(id);
|
||||
}
|
||||
|
||||
/// Returns true if the currently selected breadcrumb is the first one.
|
||||
pub fn is_top_module(&self) -> bool {
|
||||
self.currently_selected.get().is_none()
|
||||
self.selected.get() == 0
|
||||
}
|
||||
|
||||
/// Returns a currently selected breadcrumb id.
|
||||
pub fn currently_selected(&self) -> Option<component::Id> {
|
||||
self.currently_selected.get()
|
||||
/// Returns a currently selected breadcrumb id. Returns [`None`] if the top level breadcrumb
|
||||
/// is selected.
|
||||
pub fn selected(&self) -> Option<component::Id> {
|
||||
if self.is_top_module() {
|
||||
None
|
||||
} else {
|
||||
let index = self.selected.get();
|
||||
self.list.borrow().get(index - 1).map(BreadcrumbEntry::id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =======================
|
||||
// === BreadcrumbEntry ===
|
||||
// =======================
|
||||
|
||||
/// A single entry in the breadcrumbs panel.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BreadcrumbEntry {
|
||||
displayed_name: ImString,
|
||||
component_id: component::Id,
|
||||
qualified_name: QualifiedName,
|
||||
}
|
||||
|
||||
impl BreadcrumbEntry {
|
||||
/// A displayed label of the entry.
|
||||
pub fn name(&self) -> ImString {
|
||||
self.displayed_name.clone_ref()
|
||||
}
|
||||
|
||||
/// A component id of the entry.
|
||||
pub fn id(&self) -> component::Id {
|
||||
self.component_id
|
||||
}
|
||||
|
||||
/// A qualified name of the entry.
|
||||
pub fn qualified_name(&self) -> &QualifiedName {
|
||||
&self.qualified_name
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(component::Id, Rc<Entry>)> for BreadcrumbEntry {
|
||||
fn from((component_id, entry): (component::Id, Rc<Entry>)) -> Self {
|
||||
let qualified_name = entry.qualified_name();
|
||||
let displayed_name = ImString::new(&entry.name);
|
||||
BreadcrumbEntry { displayed_name, component_id, qualified_name }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ===============
|
||||
// === Builder ===
|
||||
// ===============
|
||||
|
||||
/// A builder for the breadcrumbs list. It is used to include all parent modules when pushing the
|
||||
/// new breadcrumb to the panel.
|
||||
#[derive(Debug)]
|
||||
pub struct Builder<'a> {
|
||||
database: &'a model::SuggestionDatabase,
|
||||
components: component::List,
|
||||
}
|
||||
|
||||
impl<'a> Builder<'a> {
|
||||
/// Constructor.
|
||||
pub fn new(database: &'a model::SuggestionDatabase, components: component::List) -> Self {
|
||||
Self { database, components }
|
||||
}
|
||||
|
||||
/// Build a list of breadcrumbs for a specified module. The list will contain:
|
||||
/// 1. The main module of the project.
|
||||
/// 2. All parent modules of the [`module`].
|
||||
/// 3. The [`module`] itself.
|
||||
///
|
||||
/// Returns an empty vector if the [`module`] is not found in the database or in the
|
||||
/// components list.
|
||||
pub fn build(self, module: &component::Id) -> Box<dyn Iterator<Item = BreadcrumbEntry>> {
|
||||
let (module_name, entry) = match self.module_name_and_entry(module) {
|
||||
Some(name_and_entry) => name_and_entry,
|
||||
None => return Box::new(iter::empty()),
|
||||
};
|
||||
let project_name = module_name.project_name.clone();
|
||||
let main_module_name = module::QualifiedName::new_main(project_name.clone());
|
||||
let main_module = self.lookup(&main_module_name);
|
||||
let to_main_module_entry = |entry: (component::Id, Rc<Entry>)| BreadcrumbEntry {
|
||||
displayed_name: String::from(project_name.project).into(),
|
||||
..entry.into()
|
||||
};
|
||||
let main_module = main_module.map(to_main_module_entry).into_iter();
|
||||
let parents = self.collect_parents(&module_name);
|
||||
let iter = iter::once(entry).chain(parents).chain(main_module).rev();
|
||||
Box::new(iter)
|
||||
}
|
||||
|
||||
fn module_name_and_entry(
|
||||
&self,
|
||||
module: &component::Id,
|
||||
) -> Option<(Rc<module::QualifiedName>, BreadcrumbEntry)> {
|
||||
let module_name = self.components.module_qualified_name(*module)?;
|
||||
let entry = BreadcrumbEntry::from(self.lookup(&module_name)?);
|
||||
Some((module_name, entry))
|
||||
}
|
||||
|
||||
fn lookup(&self, name: &module::QualifiedName) -> Option<(component::Id, Rc<Entry>)> {
|
||||
self.database.lookup_by_qualified_name(name.into_iter())
|
||||
}
|
||||
|
||||
/// Collect all parent modules of the given module.
|
||||
///
|
||||
/// Panics if the module is not found in the database.
|
||||
fn collect_parents(&self, name: &module::QualifiedName) -> Vec<BreadcrumbEntry> {
|
||||
let parents = name.parent_modules();
|
||||
let database_entries = parents.filter_map(|name| self.lookup(&name));
|
||||
// Note: it would be nice to avoid allocation here, but we need to reverse the
|
||||
// iterator later, so returning `impl Iterator` is not an option. We can only reverse
|
||||
// `DoubleEndedIterator`.
|
||||
database_entries.map(BreadcrumbEntry::from).collect()
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ use crate::model::suggestion_database;
|
||||
|
||||
use convert_case::Case;
|
||||
use convert_case::Casing;
|
||||
use double_representation::module;
|
||||
|
||||
|
||||
// ==============
|
||||
@ -201,8 +202,9 @@ impl Display for Component {
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Clone, CloneRef, Debug)]
|
||||
pub struct ModuleGroups {
|
||||
pub content: Group,
|
||||
pub submodules: group::AlphabeticalList,
|
||||
pub qualified_name: Rc<module::QualifiedName>,
|
||||
pub content: Group,
|
||||
pub submodules: group::AlphabeticalList,
|
||||
}
|
||||
|
||||
|
||||
@ -257,6 +259,11 @@ impl List {
|
||||
self.module_groups.get(&component).map(|mg| &mg.content)
|
||||
}
|
||||
|
||||
/// Get the qualified name of the module. Returns [`None`] if given component is not a module.
|
||||
pub fn module_qualified_name(&self, component: Id) -> Option<Rc<module::QualifiedName>> {
|
||||
self.module_groups.get(&component).map(|mg| mg.qualified_name.clone_ref())
|
||||
}
|
||||
|
||||
/// Update matching info in all components according to the new filtering pattern.
|
||||
pub fn update_filtering(&self, pattern: impl AsRef<str>) {
|
||||
let pattern = pattern.as_ref();
|
||||
|
@ -40,6 +40,7 @@ use double_representation::project;
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ModuleGroups {
|
||||
pub qualified_name: module::QualifiedName,
|
||||
pub content: component::Group,
|
||||
/// The flattened content contains the content of a module and all its submodules. Is set to
|
||||
/// `Some` only when such flattened content is needed (decided during construction).
|
||||
@ -55,20 +56,33 @@ impl ModuleGroups {
|
||||
/// Construct the builder without content nor submodules.
|
||||
///
|
||||
/// The existence of flattened content is decided during construction.
|
||||
pub fn new(component_id: component::Id, entry: &suggestion_database::Entry) -> Self {
|
||||
///
|
||||
/// Returns [`FallibleResult::Err`] if entry's qualified name can't be converted into a module
|
||||
/// name.
|
||||
pub fn new(
|
||||
component_id: component::Id,
|
||||
entry: &suggestion_database::Entry,
|
||||
) -> FallibleResult<Self> {
|
||||
let is_top_module = entry.module.is_top_module();
|
||||
let qualified_name = entry.qualified_name();
|
||||
let qualified_name = module::QualifiedName::from_all_segments(qualified_name.into_iter())?;
|
||||
let mk_group = || component::Group::from_entry(component_id, entry);
|
||||
Self {
|
||||
Ok(Self {
|
||||
qualified_name,
|
||||
content: mk_group(),
|
||||
flattened_content: is_top_module.as_some_from(mk_group),
|
||||
submodules: default(),
|
||||
is_top_module,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Build [`component::ModuleGroups`] structure with appropriately sorted submodules.
|
||||
pub fn build(self) -> component::ModuleGroups {
|
||||
component::ModuleGroups { content: self.content, submodules: self.submodules.build() }
|
||||
component::ModuleGroups {
|
||||
qualified_name: Rc::new(self.qualified_name),
|
||||
content: self.content,
|
||||
submodules: self.submodules.build(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -141,6 +155,17 @@ impl List {
|
||||
component_inserted_somewhere = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Entry has no parent module, so either it belongs to the main module of the
|
||||
// project, or it is a main module itself.
|
||||
if !entry.is_main_module() {
|
||||
let project_name = entry.module.project_name.clone();
|
||||
let main_module = module::QualifiedName::new_main(project_name);
|
||||
if let Some(main_group) = self.lookup_module_group(db, &main_module) {
|
||||
main_group.content.entries.borrow_mut().push(component.clone_ref());
|
||||
component_inserted_somewhere = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if component_inserted_somewhere {
|
||||
self.all_components.push(component);
|
||||
@ -203,11 +228,20 @@ impl List {
|
||||
if self.module_groups.contains_key(&module_id) {
|
||||
self.module_groups.get_mut(&module_id)
|
||||
} else {
|
||||
let groups = ModuleGroups::new(module_id, &*db_entry);
|
||||
let groups = ModuleGroups::new(module_id, &*db_entry).ok()?;
|
||||
if let Some(module) = module.parent_module() {
|
||||
if let Some(parent_groups) = self.lookup_module_group(db, &module) {
|
||||
parent_groups.submodules.push(groups.content.clone_ref())
|
||||
}
|
||||
} else {
|
||||
// Module has no parent, so it is a top-level module that can be added as a
|
||||
// submodule of the main module of the project.
|
||||
let main_module = module::QualifiedName::new_main(module.project_name.clone());
|
||||
if main_module != *module {
|
||||
if let Some(main_groups) = self.lookup_module_group(db, &main_module) {
|
||||
main_groups.submodules.push(groups.content.clone_ref());
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(self.module_groups.entry(module_id).or_insert(groups))
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::controller::project::INITIAL_MODULE_NAME;
|
||||
use crate::presenter::Presenter;
|
||||
|
||||
use analytics::AnonymousData;
|
||||
@ -104,8 +103,6 @@ pub struct FailedIde {
|
||||
|
||||
|
||||
/// The Path of the module initially opened after opening project in IDE.
|
||||
pub fn initial_module_path(project: &model::Project) -> FallibleResult<model::module::Path> {
|
||||
model::module::Path::from_name_segments(project.project_content_root_id(), &[
|
||||
INITIAL_MODULE_NAME,
|
||||
])
|
||||
pub fn initial_module_path(project: &model::Project) -> model::module::Path {
|
||||
project.main_module_path()
|
||||
}
|
||||
|
@ -100,27 +100,22 @@ pub trait API: Debug {
|
||||
///
|
||||
/// This module is special, as it needs to be referred by the project name itself.
|
||||
fn main_module(&self) -> model::module::QualifiedName {
|
||||
let id = controller::project::main_module_id();
|
||||
let name = self.qualified_name();
|
||||
model::module::QualifiedName::new(name, id)
|
||||
model::module::QualifiedName::new_main(name)
|
||||
}
|
||||
|
||||
// TODO [mwu] The code below likely should be preferred but does not work
|
||||
// because language server does not support using project name
|
||||
// for project's main module in some contexts.
|
||||
// This is tracked by: https://github.com/enso-org/enso/issues/1543
|
||||
// use model::module::QualifiedName;
|
||||
// ReferentName::try_from(self.name().as_str())
|
||||
// .map(QualifiedName::new_main)
|
||||
// .map_err(Into::into)
|
||||
/// Get the file path of the project's `Main` module.
|
||||
fn main_module_path(&self) -> model::module::Path {
|
||||
let main_name = self.main_module();
|
||||
let content_root_id = self.project_content_root_id();
|
||||
model::module::Path::from_id(content_root_id, &main_name.id)
|
||||
}
|
||||
|
||||
/// Get a model of the project's main module.
|
||||
#[allow(clippy::needless_lifetimes)] // Note: Needless lifetimes
|
||||
fn main_module_model<'a>(&'a self) -> BoxFuture<'a, FallibleResult<model::Module>> {
|
||||
async move {
|
||||
let main_name = self.main_module();
|
||||
let content_root_id = self.project_content_root_id();
|
||||
let main_path = model::module::Path::from_id(content_root_id, &main_name.id);
|
||||
let main_path = self.main_module_path();
|
||||
self.module(main_path).await
|
||||
}
|
||||
.boxed_local()
|
||||
|
@ -378,6 +378,14 @@ impl Entry {
|
||||
_ => Some(self.module.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this entry is a main module of the project.
|
||||
pub fn is_main_module(&self) -> bool {
|
||||
match self.kind {
|
||||
Kind::Module => self.module.is_main_module(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -19,6 +19,7 @@ use crate::presenter::graph::ViewNodeId;
|
||||
use enso_frp as frp;
|
||||
use ide_view as view;
|
||||
use ide_view::component_browser::list_panel;
|
||||
use ide_view::component_browser::list_panel::BreadcrumbId;
|
||||
use ide_view::component_browser::list_panel::EnteredModule;
|
||||
use ide_view::component_browser::list_panel::LabeledAnyModelProvider;
|
||||
use ide_view::graph_editor::component::node as node_view;
|
||||
@ -190,24 +191,47 @@ impl Model {
|
||||
}
|
||||
}
|
||||
|
||||
fn breadcrumb_selected(&self, id: BreadcrumbId) {
|
||||
self.controller.select_breadcrumb(id);
|
||||
}
|
||||
|
||||
fn set_breadcrumbs(&self, names: impl Iterator<Item = ImString>) {
|
||||
if let SearcherVariant::ComponentBrowser(browser) = self.view.searcher() {
|
||||
// We only update the breadcrumbs starting from the second element because the first
|
||||
// one is reserved as a section name.
|
||||
let from = 1;
|
||||
let breadcrumbs_from = (names.map(Into::into).collect(), from);
|
||||
browser.model().list.set_breadcrumbs_from(breadcrumbs_from);
|
||||
}
|
||||
}
|
||||
|
||||
fn show_breadcrumbs_ellipsis(&self, show: bool) {
|
||||
if let SearcherVariant::ComponentBrowser(browser) = self.view.searcher() {
|
||||
browser.model().list.show_breadcrumbs_ellipsis(show);
|
||||
}
|
||||
}
|
||||
|
||||
fn module_entered(&self, module: EnteredModule) {
|
||||
self.enter_module(module);
|
||||
}
|
||||
|
||||
fn enter_module(&self, module: EnteredModule) -> Option<()> {
|
||||
match module {
|
||||
let id = match module {
|
||||
EnteredModule::Entry(group, entry_id) => {
|
||||
let view_id = list_panel::EntryId { group, entry_id };
|
||||
let component = self.component_by_view_id(view_id)?;
|
||||
let id = component.id()?;
|
||||
self.controller.enter_module(&id);
|
||||
component.id()?
|
||||
}
|
||||
EnteredModule::Group(group_id) => {
|
||||
let group = self.group_by_view_id(group_id)?;
|
||||
let id = group.component_id?;
|
||||
self.controller.enter_module(&id);
|
||||
group.component_id?
|
||||
}
|
||||
}
|
||||
};
|
||||
self.controller.enter_module(&id);
|
||||
let names = self.controller.breadcrumbs();
|
||||
self.set_breadcrumbs(names.into_iter());
|
||||
let show_ellipsis = self.controller.last_module_has_submodules();
|
||||
self.show_breadcrumbs_ellipsis(show_ellipsis);
|
||||
Some(())
|
||||
}
|
||||
|
||||
@ -375,6 +399,7 @@ impl Searcher {
|
||||
eval_ list_view.suggestion_accepted([]analytics::remote_log_event("component_browser::suggestion_accepted"));
|
||||
eval list_view.suggestion_selected((entry) model.suggestion_selected(*entry));
|
||||
eval list_view.module_entered((id) model.module_entered(*id));
|
||||
eval list_view.selected_breadcrumb((id) model.breadcrumb_selected(*id));
|
||||
}
|
||||
}
|
||||
SearcherVariant::OldNodeSearcher(searcher) => {
|
||||
|
@ -355,7 +355,7 @@ async fn binary_visualization_updates_test_hlp() {
|
||||
use ensogl::system::web::sleep;
|
||||
|
||||
let logger = Logger::new("Test");
|
||||
let module_path = enso_gui::initial_module_path(&project).unwrap();
|
||||
let module_path = enso_gui::initial_module_path(&project);
|
||||
let method = module_path.method_pointer(project.qualified_name(), MAIN_DEFINITION_NAME);
|
||||
let module_qualified_name = project.qualified_module_name(&module_path);
|
||||
let module = project.module(module_path).await.unwrap();
|
||||
|
@ -201,9 +201,14 @@ impl EntryData {
|
||||
self.text.set_default_text_size(size);
|
||||
}
|
||||
|
||||
fn is_state_switch(&self, model: &Model) -> bool {
|
||||
fn is_state_change(&self, model: &Model) -> bool {
|
||||
match model {
|
||||
Model::Text(_) => self.state.get() != State::Text,
|
||||
Model::Text(new_text) => {
|
||||
let previous_state_was_not_text = self.state.get() != State::Text;
|
||||
let previous_text = String::from(self.text.content.value());
|
||||
let text_was_different = previous_text.as_str() != new_text.as_str();
|
||||
previous_state_was_not_text || text_was_different
|
||||
}
|
||||
Model::Separator => self.state.get() != State::Separator,
|
||||
Model::Ellipsis => self.state.get() != State::Ellipsis,
|
||||
}
|
||||
@ -284,7 +289,7 @@ impl ensogl_grid_view::Entry for Entry {
|
||||
color_anim.target <+ should_grey_out.map(|should| if *should { 1.0 } else { 0.0 });
|
||||
target_color <- all_with3(&text_color, &greyed_out_color, &color_anim.value, mix);
|
||||
appear_anim.target <+ init.constant(1.0);
|
||||
model_was_set <- input.set_model.map(f!((model) data.is_state_switch(model))).on_true();
|
||||
model_was_set <- input.set_model.map(f!((model) data.is_state_change(model))).on_true();
|
||||
should_appear <- any(&init, &model_was_set);
|
||||
eval_ should_appear({
|
||||
appear_anim.target.emit(0.0);
|
||||
|
@ -84,7 +84,8 @@ const SCROLLING_THRESHOLD_FRACTION: f32 = 0.5;
|
||||
|
||||
type GridView = grid_view::selectable::GridView<Entry>;
|
||||
type Entries = Rc<RefCell<Vec<Breadcrumb>>>;
|
||||
type BreadcrumbId = usize;
|
||||
/// The index of the breadcrumb in the list.
|
||||
pub type BreadcrumbId = usize;
|
||||
|
||||
|
||||
|
||||
@ -346,12 +347,22 @@ impl Model {
|
||||
self.grid.set_entries_params(params);
|
||||
}
|
||||
|
||||
/// Push a new breadcrumb to the top of the stack. Immediately selects added breadcrumb.
|
||||
/// A newly added breadcrumb will be placed after the currently selected one. All inactive
|
||||
/// (greyed out) breadcrumbs will be removed.
|
||||
pub fn push(&self, breadcrumb: &Breadcrumb, selected: BreadcrumbId) {
|
||||
self.entries.borrow_mut().truncate(selected + 1);
|
||||
self.entries.borrow_mut().push(breadcrumb.clone_ref());
|
||||
/// Set the breadcrumbs starting from the [`starting_from`] index. Existing entries after
|
||||
/// [`starting_from`] will be overwritten. [`self.entries`] will be extended if needed to fit
|
||||
/// all added entries.
|
||||
/// Immediately selects the last breadcrumb. All inactive (greyed out) breadcrumbs will be
|
||||
/// removed.
|
||||
pub fn set_entries(&self, starting_from: usize, new_entries: &[Breadcrumb]) {
|
||||
{
|
||||
let mut borrowed = self.entries.borrow_mut();
|
||||
let end_of_overwritten_entries = starting_from + new_entries.len();
|
||||
borrowed.truncate(end_of_overwritten_entries);
|
||||
let len = borrowed.len();
|
||||
let count_to_overwrite = len.saturating_sub(starting_from);
|
||||
let range_to_overwrite = starting_from..len;
|
||||
borrowed[range_to_overwrite].clone_from_slice(&new_entries[..count_to_overwrite]);
|
||||
borrowed.extend(new_entries.iter().map(CloneRef::clone_ref).skip(count_to_overwrite));
|
||||
}
|
||||
let new_col_count = self.grid_columns();
|
||||
self.grid.resize_grid(1, new_col_count);
|
||||
self.grid.request_model_for_visible_entries();
|
||||
@ -360,6 +371,13 @@ impl Model {
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a new breadcrumb to the top of the stack. Immediately selects added breadcrumb.
|
||||
/// A newly added breadcrumb will be placed after the currently selected one. All inactive
|
||||
/// (greyed out) breadcrumbs will be removed.
|
||||
pub fn push(&self, breadcrumb: &Breadcrumb) {
|
||||
self.set_entries(self.entries.borrow().len(), &[breadcrumb.clone_ref()]);
|
||||
}
|
||||
|
||||
/// Move the selection to the previous breadcrumb. Stops at the first one. There is always at
|
||||
/// least one breadcrumb selected.
|
||||
pub fn move_up(&self) {
|
||||
@ -412,8 +430,8 @@ ensogl_core::define_endpoints_2! {
|
||||
select(BreadcrumbId),
|
||||
/// Add a new breadcrumb after the currently selected one.
|
||||
push(Breadcrumb),
|
||||
/// Set a list of displayed breadcrumbs, rewriting any previously added breadcrumbs.
|
||||
set_entries(Vec<Breadcrumb>),
|
||||
/// Set the displayed breadcrumbs starting from the specific index.
|
||||
set_entries_from((Vec<Breadcrumb>, usize)),
|
||||
/// Enable or disable displaying of the ellipsis icon at the end of the list.
|
||||
show_ellipsis(bool),
|
||||
/// Remove all breadcrumbs.
|
||||
@ -465,10 +483,8 @@ impl Breadcrumbs {
|
||||
eval selected_grid_col(((_row, col)) model.grey_out(Some(col + 1)));
|
||||
eval_ input.clear(model.clear());
|
||||
selected <- selected_grid_col.map(|(_, col)| col / 2);
|
||||
_eval <- input.push.map2(&selected, f!((b, s) model.push(b, *s)));
|
||||
entries <= input.set_entries.map(f!((e) { model.clear(); e.clone() }));
|
||||
_eval <- entries.map2(&selected, f!((entry, s) model.push(entry, *s)));
|
||||
eval selected([](id) tracing::debug!("Selected breadcrumb: {id}"));
|
||||
eval input.push((b) model.push(b));
|
||||
eval input.set_entries_from(((entries, from)) model.set_entries(*from, entries));
|
||||
out.selected <+ selected;
|
||||
|
||||
scroll_anim.target <+ all_with3(&model.grid.content_size, &input.set_size, &model.grid
|
||||
|
@ -95,6 +95,7 @@ pub mod layouting;
|
||||
|
||||
mod navigator;
|
||||
|
||||
pub use breadcrumbs::BreadcrumbId;
|
||||
pub use column_grid::LabeledAnyModelProvider;
|
||||
pub use component_group::set::EnteredModule;
|
||||
pub use component_group::set::GroupId;
|
||||
@ -463,7 +464,6 @@ impl Model {
|
||||
breadcrumbs.set_base_layer(&layers.navigator);
|
||||
display_object.add_child(&breadcrumbs);
|
||||
breadcrumbs.show_ellipsis(true);
|
||||
breadcrumbs.set_entries(vec![breadcrumbs::Breadcrumb::new("All")]);
|
||||
|
||||
let selection = selection_box::View::new(&app.logger);
|
||||
scroll_area.add_child(&selection);
|
||||
@ -512,6 +512,11 @@ impl Model {
|
||||
LabeledSection::new(content, app)
|
||||
}
|
||||
|
||||
fn set_initial_breadcrumbs(&self) {
|
||||
self.breadcrumbs.set_entries_from((vec![breadcrumbs::Breadcrumb::new("All")], 0));
|
||||
self.breadcrumbs.show_ellipsis(true);
|
||||
}
|
||||
|
||||
fn update_style(&self, style: &Style) {
|
||||
// Background
|
||||
|
||||
@ -935,6 +940,10 @@ define_endpoints_2! {
|
||||
set_local_scope_section(list_view::entry::AnyModelProvider<component_group::Entry>),
|
||||
set_favourites_section(Vec<LabeledAnyModelProvider>),
|
||||
set_sub_modules_section(Vec<LabeledAnyModelProvider>),
|
||||
/// See [`breadcrumbs::Breadcrumb::Frp::set_entries_from`].
|
||||
set_breadcrumbs_from((Vec<breadcrumbs::Breadcrumb>, usize)),
|
||||
/// Show or hide the ellipsis at the end of the breadcrumbs list.
|
||||
show_breadcrumbs_ellipsis(bool),
|
||||
/// The component browser is displayed on screen.
|
||||
show(),
|
||||
/// The component browser is hidden from screen.
|
||||
@ -948,6 +957,7 @@ define_endpoints_2! {
|
||||
/// The last selected suggestion.
|
||||
suggestion_selected(EntryId),
|
||||
size(Vector2),
|
||||
selected_breadcrumb(BreadcrumbId),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1069,6 +1079,14 @@ impl component::Frp<Model> for Frp {
|
||||
let weak_color = style.get_color(list_panel_theme::navigator_icon_weak_color);
|
||||
let params = icon::Params { strong_color, weak_color };
|
||||
model.section_navigator.set_bottom_buttons_entry_params(params);
|
||||
|
||||
|
||||
// === Breadcrumbs ===
|
||||
|
||||
model.breadcrumbs.set_entries_from <+ input.set_breadcrumbs_from;
|
||||
model.breadcrumbs.show_ellipsis <+ input.show_breadcrumbs_ellipsis;
|
||||
output.selected_breadcrumb <+ model.breadcrumbs.selected;
|
||||
eval_ input.show(model.set_initial_breadcrumbs());
|
||||
}
|
||||
layout_frp.init.emit(());
|
||||
selection_animation.skip.emit(());
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Options intended to be common for all developers.
|
||||
|
||||
wasm-size-limit: 14.79 MiB
|
||||
wasm-size-limit: 14.80 MiB
|
||||
|
||||
required-versions:
|
||||
cargo-watch: ^8.1.1
|
||||
|
@ -76,10 +76,18 @@ impl ColumnWidths {
|
||||
/// `colunm` can be equal to `number_of_columns` passed to the [`Self::new`] or
|
||||
/// [`Self::resize`].
|
||||
pub fn pos_offset(&self, column: usize) -> f32 {
|
||||
let borrowed = self.width_diffs.borrow();
|
||||
let len = borrowed.len();
|
||||
if column == 0 {
|
||||
0.0
|
||||
} else if column > len {
|
||||
tracing::warn!(
|
||||
"Trying to get a position offset of a column that does not exist. \
|
||||
{column} > {len}. Returning 0.0."
|
||||
);
|
||||
0.0
|
||||
} else {
|
||||
self.width_diffs.borrow().query(column - 1)
|
||||
borrowed.query(column - 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -304,6 +304,12 @@ macro_rules! im_string_newtype_without_serde {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ImString> for $name {
|
||||
fn from(t:ImString) -> Self {
|
||||
Self::new(t)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for $name {
|
||||
fn from(t:&str) -> Self {
|
||||
Self::new(t)
|
||||
|
Loading…
Reference in New Issue
Block a user