Published on to joshleeb's blog
G’day!
This month I’ve been working on Ohm, a fuzzy finder based on Orderless. I introduced Ohm in the last update, and I’ll be writing a dedicated post including motivations and usage examples when it’s ready for use.
So far, the core library is functionally complete but needs more documentation. Progress on an FZF-like command line tool is also underway which I expect to be usable for the next status update.
Unsurprisingly, many of the concepts from Orderless have mapped directly onto Ohm, but the shape has changed a fair bit.
Matchers
At the center of Orderless are Component Matching Styles.
These are functions that mostly map a string (the component) to a regex. For
example, the orderless-flex
matching style would return a.*b.*c
for the
component abc
. This regex is then evaluated over all the candidate strings
to produce a set of matches.
Ohm takes a different approach. Instead of producing a regex for use
downstream, it defines matchers that find a component in a candidate and
optionally return a match, as modeled by the Matcher
trait.
trait Matcher {
fn find(&self, component: &str, candidate: &str) -> Option<Match>;
}
The idea behind this change is that we don’t always want to match with
regexes. E.g: in the case of LiteralMatcher
it’s simpler (and faster?) to
perform substring search instead of compiling and evaluating a regex.
Transformers
The next kind of Orderless components are Style Modifiers
which are functions that take a predicate function and a regex, and return a
new predicate function that indicates whether a match was found. For example,
orderless-not
which inverts the matching regex.
Another way to think about modifiers is as matcher transformers. That is, a function takes a matcher as input and produce a new matcher. In Ohm that is exactly how they are modeled.
trait Transformer {
fn apply(&self, matcher: Arc<dyn Matcher>) -> Arc<dyn Matcher>;
}
Implementing orderless-not
(called inverse
in Ohm) as a transformer is
trivial, and quite a bit more flexible and ergonomic.
fn inverse(matcher: Arc<dyn Matcher>) -> Arc<dyn Matcher> {
Arc::new(move |component: &str, candidate: &str| {
matcher.find(component, candidate).map(|m| match m {
Some(_) => None,
None => Some(Match::new(...)),
})
})
}
Predicates & Dispatching
Lastly, Orderless has Style Dispatchers. These are functions that, given a component, will decide which (if any) matching style to use and modify the component to pass to that style.
In Emacs, the dispatcher I would invoke most often is shown in code 4. This
function dispatches to the orderless-flex
matching style if the component
starts with a ‘~’ prefix, and forwards the component without that prefix.
(defun flex-if-tilde (component _index _total)
(when (string-prefix-p "~" component)
`(orderless-flex . ,(substring pattern 1))))
Again, Ohm takes a different approach, opting to split Orderless dispatchers into a predicate and a dispatching matcher.
trait Predicate {
fn check(&self, component: &str, position: Position) -> Option<String>;
}
Predicates in Ohm are very simple. They take a component and its position
(e.g: second component out of five) and return the modified component if the
predicate succeeds, otherwise None
. This change allows for more of a general
implementation, such as in code 6 with the PrefixPredicate
.
struct PrefixPredicate(char);
impl Predicate for PrefixPredicate {
fn check(&self, component: &str, _position: Position) -> Option<String> {
component.strip_prefix(self.0).map(String::from)
}
}
Predicates on their own aren’t very useful. They are intended to be used as
part of a matcher (or transformer). For example, DispatchMatcher
where a
matcher is dispatched for the first successful predicate, otherwise the
fallback is used.
struct DispatchMatcher {
entries: Vec<(Arc<dyn Predicate>, Arc<dyn Matcher>)>,
fallback: Arc<dyn Matcher>,
}
let root = DispatchMatcher::builder()
.with(PrefixPredicate('='), LiteralMatcher::default())
.with(PrefixPredicate('~'), FlexMatcher::default())
.build(RegexMatcher::default());
You can see with the Arc
s there’s a bit more machinery and trait
implementations to improve the ergonomics and support using Ohm across threads
(i.e. implementing Send + Sync
). For the most part though, that’s everything
in the core of Ohm.
Wrapping Up
Next I’ll be working on a command line fuzzy finder that makes use of Ohm core and has a very similar interface to FZF. However, I’ll be taking a break from personal projects and everything else in September as I am getting married (!!!)
That’s all for now. See you in October!