Electron Adventures: Episode 38: Command Palette Highlighting
Tags: javascript, electron, svelte
Published: 2021-08-30 04:12:39

In previous episode we added a very simple command palette to the file manager. Over this and next few episodes we'll be improving on it. The first feature to add - match highlighting.

### Why we need highlighting

It might seed like just an aesthetic issue, but it isn't. If user search for `go`, and matches are:

* Go to First File
* Go to Last File
* Go to Next File
* Go to Previous File
* Page Down

It might be very baffling why that last one is there ("pa**G**e d**O**wn"). Especially if for any reason the unexpected match takes priority over expected matches. Any such confusion can break the user out of the flow state.

### `src/CommandPalette.svelte`

`CommandPalette` stops being responsible for filtering commands, all the responsibility will move to `matcher.js`

```javascript
  import matcher from "./matcher.js"
  $: matchingCommands = matcher(commands, pattern)
```

### `src/matcher.js`

This is a fairly simple implementation, even if it uses a lot of `RegExp` trickery.

* first we turn pattern into all lower case and strip everything that's not letter or number
* every letter in the pattern we turn into regular expression, for example `x`  becomes `/(.*?)(x)(.*)/i` - that is first parenthesis will match everything left of "x", second will match "x" (case insensitive), third everything to the right of "x" - if there are multiple "x"s, we only match the first one. That's what the question mark is for, to stop as soon as possible, by default regular expressions keep going as far as possible.
* then we loop over all commands calling `checkMatch` - if it matches, we add it to results together with the match, otherwise we don't add it to the result


```javascript
function matcher(commands, pattern) {
  let rxs = pattern
    .toLowerCase()
    .replace(/[^a-z0-9]/, "")
    .split("")
    .map(l => new RegExp(`(.*?)(${l})(.*)`, "i"))
  let result = []
  for (let command of commands) {
    let match = checkMatch(rxs, command.name)
    if (match) {
      result.push({...command, match: match})
    }
  }
  return result
}

export default matcher
```

In `checkMatch`, we slice the name one letter at a time. For example if we match "Page Down" against "go", the first iteration will be:

* `"Page Down"` becomes `["Pa", "g", "e Down"]`
* `["Pa", false]` is added to result, so it won't be highlighted
* `["g", true]` is added to result, so it will be highlighted
* only `"e Down"` goes to next iteration

Then in second iteration:

* `"e Down"` becomes `["e D", "o", "wn"]`
* `["e D", false]` is added to result, so it won't be highlighted
* `["o", true]` is added to result, so it will be highlighted
* only `"wn"` remains after the loop, and whatever's left is added to the result non-highlighted as `["wn", false]`

Here's the code:

```javascript
function checkMatch(rxs, name) {
  if (!name) {
    return
  }
  let result = []
  for (let rx of rxs) {
    let m = rx.exec(name)
    if (m) {
      result.push([m[1], false])
      result.push([m[2], true])
      name = m[3]
    } else {
      return null
    }
  }
  result.push([name, false])
  return result
}
```

This would be somewhat more concide in a language with more powerful regular expressions like Ruby or even Perl, but it's not too bad.

### `src/CommandPaletteEntry.svelte`

And finally we need to add support for displaying highlighted results to `CommandPaletteEntry`.

```html
<script>
  import { getContext } from "svelte"
  let { eventBus } = getContext("app")

  export let name
  export let match = undefined
  export let key
  export let action

  function handleClick() {
    eventBus.emit("app", "closePalette")
    eventBus.emit(...action)
  }
  function keyName(key) {
    if (key === " ") {
      return "Space"
    } else {
      return key
    }
  }
</script>

<li on:click={handleClick}>
  <span class="name">
    {#if match}
      {#each match as [part, highlight]}
        {#if highlight}
          <em>{part}</em>
        {:else}
          {part}
        {/if}
      {/each}
    {:else}
      {name}
    {/if}
  </span>
  {#if key}
    <span class="key">{keyName(key)}</span>
  {/if}
</li>

<style>
  li {
    display: flex;
    padding: 0px 8px;
  }
  li:first-child {
    background-color: #66b;
  }
  .name {
    flex: 1;
  }
  .key {
    display: inline-block;
    background-color: hsl(180,100%,30%);
    padding: 2px;
    border: 1px solid  hsl(180,100%,20%);
    border-radius: 20%;
  }
  .name em {
    color: #ff2;
    font-weight: bold;
    font-style: normal;
  }
</style>
```

There's one extra optional property `match`. If it's there, we loop over it treating it as array of `[part, highlight]`. Highlighted parts are wrapped in `<em>` which is then formatted below to be highlighted in same style as selected files.

This highlighting is not quite as visible as I hoped, so at some point I'll need to adjust the styling.

### Result

Here's the results:

![Episode 38 Screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f8b9qt4hi5dz379rf21b.png)

This was a nice small feature. In the next episode we'll teach our app how to deal with modifier keys like Control, Command, Shift, and so on, so keyboard shortcuts can be more than one key.

As usual, [all the code for the episode is here](https://github.com/taw/electron-adventures/tree/master/episode-38-command-palette-highlighting).