Skip to content

Commit

Permalink
Match component browser entries by alias (#5678)
Browse files Browse the repository at this point in the history
Closes #5340

This PR adds matching searched component browser entries by alias. Now the searcher input is also matched to the `ALIAS` tags of a component, and the best match is used for filtering and sorting the components in the component browser. The alias match scores are reduced by a factor to give them a lower priority when sorting filtered entries in the component browser.

Multiple aliases for a single entry can be obtained from either multiple `ALIAS` tags in the documentation, or comma-separated aliases inside one `ALIAS` tag.

When the searcher input matches one of the entry's aliases the entry in the component browser is displayed as `alias (label)`.

https://user-images.githubusercontent.com/117099775/220571385-d6c2aba6-f13b-4517-9cdf-fe146eeb751a.mp4
  • Loading branch information
galin-enso authored Feb 23, 2023
1 parent ed6d3d0 commit b8158d0
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 25 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@
- [Improved argument placeholder resolution in more complex expressions][5656].
It is now possible to drop node connections onto missing arguments of chained
and nested function calls.
- [The component browser suggestions take into account entry aliases][5678]. The
searcher input is now matched to entry aliases too. The alias match is used to
filter and sort component browser entries.

#### EnsoGL (rendering engine)

Expand Down Expand Up @@ -492,6 +495,7 @@
[5656]: https://github.com/enso-org/enso/pull/5656
[5679]: https://github.com/enso-org/enso/pull/5679
[5699]: https://github.com/enso-org/enso/pull/5699
[5678]: https://github.com/enso-org/enso/pull/5678
[5721]: https://github.com/enso-org/enso/pull/5721

#### Enso Compiler
Expand Down
3 changes: 3 additions & 0 deletions app/gui/language/ast/impl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ pub mod constants {

/// The tag name of documentation sections marked as "PRIVATE"
pub const PRIVATE_DOC_SECTION_TAG_NAME: &str = "PRIVATE";

/// The tag name of a documentation section with a method alias.
pub const ALIAS_DOC_SECTION_TAG_NAME: &str = "ALIAS";
}

pub use crumbs::Crumb;
Expand Down
27 changes: 21 additions & 6 deletions app/gui/src/controller/searcher/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,17 +156,29 @@ pub struct Subcategory {
// === List Entry ===
// ==================

/// Which part of the component browser entry was best matched to the searcher input.
#[derive(Clone, Debug, Default)]
pub enum MatchKind {
/// The entry's label to be displayed in the component browser was matched.
#[default]
Label,
/// The code to be generated by the entry was matched.
Code,
/// An alias of the entry was matched, contains the specific alias that was matched.
Alias(ImString),
}

/// Information how the list entry matches the filtering pattern.
#[allow(missing_docs)]
#[derive(Clone, Debug)]
pub enum MatchInfo {
DoesNotMatch,
Matches { subsequence: fuzzly::Subsequence },
Matches { subsequence: fuzzly::Subsequence, kind: MatchKind },
}

impl Default for MatchInfo {
fn default() -> Self {
Self::Matches { subsequence: default() }
Self::Matches { subsequence: default(), kind: default() }
}
}

Expand All @@ -180,7 +192,8 @@ impl Ord for MatchInfo {
(DoesNotMatch, DoesNotMatch) => Equal,
(DoesNotMatch, Matches { .. }) => Less,
(Matches { .. }, DoesNotMatch) => Greater,
(Matches { subsequence: lhs }, Matches { subsequence: rhs }) => lhs.compare_scores(rhs),
(Matches { subsequence: lhs, .. }, Matches { subsequence: rhs, .. }) =>
lhs.compare_scores(rhs),
}
}
}
Expand Down Expand Up @@ -218,7 +231,7 @@ impl ListEntry {
fuzzly::find_best_subsequence(self.action.to_string(), pattern, metric)
});
self.match_info = match subsequence {
Some(subsequence) => MatchInfo::Matches { subsequence },
Some(subsequence) => MatchInfo::Matches { subsequence, kind: MatchKind::Code },
None => MatchInfo::DoesNotMatch,
};
}
Expand Down Expand Up @@ -455,7 +468,8 @@ impl<'a> CategoryBuilder<'a> {
let built_list = &self.list_builder.built_list;
let category = self.category_id;
built_list.entries.borrow_mut().extend(iter.into_iter().map(|action| {
let match_info = MatchInfo::Matches { subsequence: default() };
let match_info =
MatchInfo::Matches { subsequence: default(), kind: MatchKind::Code };
ListEntry { category, match_info, action }
}));
}
Expand Down Expand Up @@ -509,7 +523,8 @@ impl ListWithSearchResultBuilder {
.borrow()
.iter()
.map(|entry| {
let match_info = MatchInfo::Matches { subsequence: default() };
let match_info =
MatchInfo::Matches { subsequence: default(), kind: MatchKind::Code };
let action = entry.action.clone_ref();
let category = self.search_result_category;
ListEntry { category, match_info, action }
Expand Down
78 changes: 63 additions & 15 deletions app/gui/src/controller/searcher/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::prelude::*;

use crate::model::suggestion_database;

use controller::searcher::action::MatchKind;
use convert_case::Case;
use convert_case::Casing;
use double_representation::name::QualifiedName;
Expand All @@ -24,6 +25,16 @@ pub use group::Group;



// =================
// === Constants ===
// =================

/// A factor to multiply a component's alias match score by. It is intended to reduce the importance
/// of alias matches compared to label matches.
const ALIAS_MATCH_ATTENUATION_FACTOR: f32 = 0.75;



// ====================
// === Type Aliases ===
// ====================
Expand Down Expand Up @@ -115,7 +126,12 @@ impl Component {

/// The label which should be displayed in the Component Browser.
pub fn label(&self) -> String {
self.to_string()
match &*self.match_info.borrow() {
MatchInfo::Matches { kind: MatchKind::Alias(alias), .. } => {
format!("{alias} ({self})")
}
_ => self.to_string(),
}
}

/// The name of the component.
Expand Down Expand Up @@ -153,12 +169,14 @@ impl Component {
/// It should be called each time the filtering pattern changes.
pub fn update_matching_info(&self, pattern: impl Str) {
// Match the input pattern to the component label.
let label = self.label();
let label = self.to_string();
let label_matches = fuzzly::matches(&label, pattern.as_ref());
let label_subsequence = label_matches.and_option_from(|| {
let metric = fuzzly::metric::default();
fuzzly::find_best_subsequence(label, pattern.as_ref(), metric)
});
let label_match_info = label_subsequence
.map(|subsequence| MatchInfo::Matches { subsequence, kind: MatchKind::Label });

// Match the input pattern to the code to be inserted.
let code = match &self.data {
Expand All @@ -170,22 +188,34 @@ impl Component {
let metric = fuzzly::metric::default();
fuzzly::find_best_subsequence(code, pattern.as_ref(), metric)
});
let code_match_info = code_subsequence.map(|subsequence| {
let subsequence = fuzzly::Subsequence { indices: Vec::new(), ..subsequence };
MatchInfo::Matches { subsequence, kind: MatchKind::Code }
});

// Pick the best match score of the two, use only the character indices matching the label.
let subsequence = match (label_subsequence, code_subsequence) {
(Some(label), Some(code)) => {
let score = label.score.max(code.score);
Some(fuzzly::Subsequence { score, ..label })
// Match the input pattern to an entry's aliases and select the best alias match.
let alias_matches = self.aliases().filter_map(|alias| {
if fuzzly::matches(alias, pattern.as_ref()) {
let metric = fuzzly::metric::default();
let subsequence = fuzzly::find_best_subsequence(alias, pattern.as_ref(), metric);
subsequence.map(|subsequence| (subsequence, alias))
} else {
None
}
(None, Some(code)) => Some(fuzzly::Subsequence { indices: Vec::new(), ..code }),
(Some(label), None) => Some(label),
(None, None) => None,
};
});
let alias_match = alias_matches.max_by(|(lhs, _), (rhs, _)| lhs.compare_scores(rhs));
let alias_match_info = alias_match.map(|(subsequence, alias)| {
let subsequence = fuzzly::Subsequence {
score: subsequence.score * ALIAS_MATCH_ATTENUATION_FACTOR,
..subsequence
};
MatchInfo::Matches { subsequence, kind: MatchKind::Alias(alias.to_im_string()) }
});

*self.match_info.borrow_mut() = match subsequence {
Some(subsequence) => MatchInfo::Matches { subsequence },
None => MatchInfo::DoesNotMatch,
};
// Select the best match of the available label-, code- and alias matches.
let match_info_iter = [alias_match_info, code_match_info, label_match_info].into_iter();
let best_match_info = match_info_iter.flatten().max_by(|lhs, rhs| lhs.cmp(rhs));
*self.match_info.borrow_mut() = best_match_info.unwrap_or(MatchInfo::DoesNotMatch);
}

/// Check whether the component contains the "PRIVATE" tag.
Expand All @@ -199,6 +229,24 @@ impl Component {
_ => false,
}
}

/// Return an iterator over the component's aliases from the "ALIAS" tags in the entry's
/// documentation.
pub fn aliases(&self) -> impl Iterator<Item = &str> {
let aliases = match &self.data {
Data::FromDatabase { entry, .. } => {
let aliases = entry.documentation.iter().filter_map(|doc| match doc {
DocSection::Tag { name, body }
if name == ast::constants::ALIAS_DOC_SECTION_TAG_NAME =>
Some(body.as_str().split(',').map(|s| s.trim())),
_ => None,
});
Some(aliases.flatten())
}
_ => None,
};
aliases.into_iter().flatten()
}
}

impl From<Rc<hardcoded::Snippet>> for Component {
Expand Down
6 changes: 4 additions & 2 deletions app/gui/src/controller/searcher/component/group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,10 @@ impl Group {
(MatchInfo::DoesNotMatch, MatchInfo::DoesNotMatch) => cmp::Ordering::Equal,
(MatchInfo::DoesNotMatch, MatchInfo::Matches { .. }) => cmp::Ordering::Greater,
(MatchInfo::Matches { .. }, MatchInfo::DoesNotMatch) => cmp::Ordering::Less,
(MatchInfo::Matches { subsequence: lhs }, MatchInfo::Matches { subsequence: rhs }) =>
lhs.compare_scores(rhs),
(
MatchInfo::Matches { subsequence: lhs, .. },
MatchInfo::Matches { subsequence: rhs, .. },
) => lhs.compare_scores(rhs),
}
}

Expand Down
4 changes: 2 additions & 2 deletions app/gui/src/presenter/searcher/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ impl list_view::entry::ModelProvider<GlyphHighlightedLabel> for Action {

fn get(&self, id: usize) -> Option<list_view::entry::GlyphHighlightedLabelModel> {
let action = self.actions.get_cloned(id)?;
if let MatchInfo::Matches { subsequence } = action.match_info {
if let MatchInfo::Matches { subsequence, .. } = action.match_info {
let label = action.action.to_string();
let mut char_iter = label.char_indices().enumerate();
let highlighted = subsequence
Expand Down Expand Up @@ -333,7 +333,7 @@ fn component_to_entry_model(component: &component::Component) -> component_grid:
}

fn bytes_of_matched_letters(match_info: &MatchInfo, label: &str) -> Vec<text::Range<text::Byte>> {
if let MatchInfo::Matches { subsequence } = match_info {
if let MatchInfo::Matches { subsequence, .. } = match_info {
let mut char_iter = label.char_indices().enumerate();
subsequence
.indices
Expand Down

0 comments on commit b8158d0

Please sign in to comment.