Skip to content

Commit

Permalink
Updated command palette matching algorithm (#175)
Browse files Browse the repository at this point in the history
- Suppressed `IDE0045: Convert to conditional expression`.

## Core

- Added `VeryObservableCollection`, which listens changes in its children.
- Renamed `KeybindManager` to `KeybindHook`, as its primary purpose is to listen to keyboard events.
- Updated descriptiveness of `KeybindsMap`.
- Removed `IKeybindManager`, as it's superfluous.
- Updated the core to deal with `CommandItem` instead of `(ICommand, IKeybind?)`.
- Updated `ICommandItems` to be `ICommandKeybindDictionary`.
- Fixed renaming of a workspace.
- Added a few tests for `Workspace`.

## Bar

- The bar will show the updated name of a workspace.

## Palette

- Ported [filters from Visual Studio Code](https://github.com/microsoft/vscode/blob/bd782eb059e133d3a20fdb446b8feb0010a278ad/src/vs/base/common/filters.ts) for text matching.
- Removed `MostOftenUsedMatcher`.
- The height, width, and y-position of the palette can be configured.
- The palette can be activated in free text or menu mode.
- Added a command to rename a workspace.
- The palette no stays at its max height when there are less items than height.
- Fixed styling binding issue.
  • Loading branch information
dalyIsaac authored Oct 10, 2022
1 parent 91d6e1c commit c4c8f4b
Show file tree
Hide file tree
Showing 61 changed files with 1,764 additions and 674 deletions.
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -256,3 +256,6 @@ dotnet_diagnostic.IDE0072.severity = silent

# IDE0010: Add missing cases
dotnet_diagnostic.IDE0010.severity = silent

# IDE0045: Convert to conditional expression
dotnet_diagnostic.IDE0045.severity = silent
5 changes: 4 additions & 1 deletion src/Whim.Bar/BarConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,10 @@ public Thickness Margin
}
}

/// <inheritdoc/>
/// <summary>
/// Handler to call when a property changes.
/// </summary>
/// <param name="propertyName">The name of the property that changed.</param>
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
Expand Down
2 changes: 1 addition & 1 deletion src/Whim.Bar/BarPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,5 +109,5 @@ public void Dispose()
}

/// <inheritdoc />
public (ICommand, IKeybind?)[] GetCommands() => Array.Empty<(ICommand, IKeybind?)>();
public CommandItem[] GetCommands() => Array.Empty<CommandItem>();
}
10 changes: 10 additions & 0 deletions src/Whim.Bar/Workspace/WorkspaceModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,14 @@ protected virtual void OnPropertyChanged(string? propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

/// <summary>
/// Triggered when the workspace is renamed.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
internal void Workspace_Renamed(object? sender, WorkspaceRenamedEventArgs e)
{
OnPropertyChanged(nameof(Name));
}
}
15 changes: 13 additions & 2 deletions src/Whim.Bar/Workspace/WorkspaceWidgetViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;

Expand All @@ -21,7 +20,7 @@ public class WorkspaceWidgetViewModel : INotifyPropertyChanged, IDisposable
/// <summary>
/// The workspaces for the monitor.
/// </summary>
public ObservableCollection<WorkspaceModel> Workspaces { get; } = new();
public VeryObservableCollection<WorkspaceModel> Workspaces { get; } = new();

/// <summary>
/// Creates a new instance of <see cref="WorkspaceWidgetViewModel"/>.
Expand All @@ -36,6 +35,7 @@ public WorkspaceWidgetViewModel(IConfigContext configContext, IMonitor monitor)
_configContext.WorkspaceManager.WorkspaceAdded += WorkspaceManager_WorkspaceAdded;
_configContext.WorkspaceManager.WorkspaceRemoved += WorkspaceManager_WorkspaceRemoved;
_configContext.WorkspaceManager.MonitorWorkspaceChanged += WorkspaceManager_MonitorWorkspaceChanged;
_configContext.WorkspaceManager.WorkspaceRenamed += WorkspaceManager_WorkspaceRenamed;

// Populate the list of workspaces
foreach (IWorkspace workspace in _configContext.WorkspaceManager)
Expand Down Expand Up @@ -99,6 +99,17 @@ private void WorkspaceManager_MonitorWorkspaceChanged(object? sender, MonitorWor
}
}

private void WorkspaceManager_WorkspaceRenamed(object? sender, WorkspaceRenamedEventArgs e)
{
WorkspaceModel? workspace = Workspaces.FirstOrDefault(m => m.Workspace == e.Workspace);
if (workspace == null)
{
return;
}

workspace.Workspace_Renamed(sender, e);
}

/// <inheritdoc/>
protected virtual void Dispose(bool disposing)
{
Expand Down
42 changes: 42 additions & 0 deletions src/Whim.CommandPalette.Tests/Filters/CamelCaseTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Xunit;

namespace Whim.CommandPalette.Tests;

public class CamelCaseTests
{
[InlineData("", "anything")]
[InlineData("alpha", "alpha", new int[] { 0, 5 })]
[InlineData("AlPhA", "alpha", new int[] { 0, 5 })]
[InlineData("alpha", "alphasomething", new int[] { 0, 5 })]
[InlineData("c", "CamelCaseRocks", new int[] { 0, 1 })]
[InlineData("cc", "CamelCaseRocks", new int[] { 0, 1 }, new int[] { 5, 6 })]
[InlineData("ccr", "CamelCaseRocks", new int[] { 0, 1 }, new int[] { 5, 6 }, new int[] { 9, 10 })]
[InlineData("cacr", "CamelCaseRocks", new int[] { 0, 2 }, new int[] { 5, 6 }, new int[] { 9, 10 })]
[InlineData("cacar", "CamelCaseRocks", new int[] { 0, 2 }, new int[] { 5, 7 }, new int[] { 9, 10 })]
[InlineData("ccarocks", "CamelCaseRocks", new int[] { 0, 1 }, new int[] { 5, 7 }, new int[] { 9, 14 })]
[InlineData("cr", "CamelCaseRocks", new int[] { 0, 1 }, new int[] { 9, 10 })]
[InlineData("fba", "FooBarAbe", new int[] { 0, 1 }, new int[] { 3, 5 })]
[InlineData("fbar", "FooBarAbe", new int[] { 0, 1 }, new int[] { 3, 6 })]
[InlineData("fbara", "FooBarAbe", new int[] { 0, 1 }, new int[] { 3, 7 })]
[InlineData("fbaa", "FooBarAbe", new int[] { 0, 1 }, new int[] { 3, 5 }, new int[] { 6, 7 })]
[InlineData("fbaab", "FooBarAbe", new int[] { 0, 1 }, new int[] { 3, 5 }, new int[] { 6, 8 })]
[InlineData("c2d", "canvasCreation2D", new int[] { 0, 1 }, new int[] { 14, 16 })]
[InlineData("cce", "_canvasCreationEvent", new int[] { 1, 2 }, new int[] { 7, 8 }, new int[] { 15, 16 })]
[InlineData("Debug Console", "Open: Debug Console", new int[] { 6, 19 })]
[InlineData("Debug console", "Open: Debug Console", new int[] { 6, 19 })]
[InlineData("debug console", "Open: Debug Console", new int[] { 6, 19 })]
[Theory]
public void MatchesCamelCase_Ok(string word, string wordToMatchAgainst, params int[][] expected)
{
PaletteFilterTextMatch[] expectedMatches = FilterTestUtils.CreateExpectedMatches(expected);
FilterTestUtils.FilterOk(PaletteFilters.MatchesCamelCase, word, wordToMatchAgainst, expectedMatches);
}

[InlineData("", "")]
[InlineData("alpha", "alph")]
[Theory]
public void MatchesCamelCase_NotOk(string word, string wordToMatchAgainst)
{
FilterTestUtils.FilterNotOk(PaletteFilters.MatchesCamelCase, word, wordToMatchAgainst);
}
}
50 changes: 50 additions & 0 deletions src/Whim.CommandPalette.Tests/Filters/PrefixTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Xunit;

namespace Whim.CommandPalette.Tests;

public class PrefixTests
{
[InlineData("", "anything")]
[InlineData("alpha", "alpha", new int[] { 0, 5 })]
[InlineData("alpha", "alphasomething", new int[] { 0, 5 })]
[InlineData("a", "alpha", new int[] { 0, 1 })]
[Theory]
public void MatchesStrictPrefix_Ok(string word, string wordToMatchAgainst, params int[][] expected)
{
PaletteFilterTextMatch[] expectedMatches = FilterTestUtils.CreateExpectedMatches(expected);
FilterTestUtils.FilterOk(PaletteFilters.MatchesStrictPrefix, word, wordToMatchAgainst, expectedMatches);
}

[InlineData("", "")]
[InlineData("alpha", "alp")]
[InlineData("x", "alpha")]
[InlineData("A", "alpha")]
[InlineData("AlPh", "alPHA")]
[Theory]
public void MatchesStrictPrefix_NotOk(string word, string wordToMatchAgainst)
{
FilterTestUtils.FilterNotOk(PaletteFilters.MatchesStrictPrefix, word, wordToMatchAgainst);
}

[InlineData("alpha", "alpha", new int[] { 0, 5 })]
[InlineData("alpha", "alphasomething", new int[] { 0, 5 })]
[InlineData("a", "alpha", new int[] { 0, 1 })]
[InlineData("ä", "Älpha", new int[] { 0, 1 })]
[InlineData("A", "alpha", new int[] { 0, 1 })]
[InlineData("AlPh", "alPHA", new int[] { 0, 4 })]
[Theory]
public void MatchesPrefix_Ok(string word, string wordToMatchAgainst, params int[][] expected)
{
PaletteFilterTextMatch[] expectedMatches = FilterTestUtils.CreateExpectedMatches(expected);
FilterTestUtils.FilterOk(PaletteFilters.MatchesPrefix, word, wordToMatchAgainst, expectedMatches);
}

[InlineData("alpha", "alp")]
[InlineData("x", "alpha")]
[InlineData("T", "4")]
[Theory]
public void MatchesPrefix_NotOk(string word, string wordToMatchAgainst)
{
FilterTestUtils.FilterNotOk(PaletteFilters.MatchesPrefix, word, wordToMatchAgainst);
}
}
31 changes: 31 additions & 0 deletions src/Whim.CommandPalette.Tests/Filters/SubstringTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Xunit;

namespace Whim.CommandPalette.Tests;

public class SubstringTests
{
[InlineData("cela", "cancelAnimationFrame()", new int[] { 3, 7 })]
[Theory]
public void MatchesContiguousSubString_Ok(string word, string wordToMatchAgainst, params int[][] expected)
{
PaletteFilterTextMatch[] expectedMatches = FilterTestUtils.CreateExpectedMatches(expected);
FilterTestUtils.FilterOk(PaletteFilters.MatchesContiguousSubString, word, wordToMatchAgainst, expectedMatches);
}

[InlineData("cmm", "cancelAnimationFrame()", new int[] { 0, 1 }, new int[] { 9, 10 }, new int[] { 18, 19 })]
[InlineData("abc", "abcabc", new int[] { 0, 3 })]
[InlineData("abc", "aaabbbccc", new int[] { 0, 1 }, new int[] { 3, 4 }, new int[] { 6, 7 })]
[Theory]
public void MatchesSubString_Ok(string word, string wordToMatchAgainst, params int[][] expected)
{
PaletteFilterTextMatch[] expectedMatches = FilterTestUtils.CreateExpectedMatches(expected);
FilterTestUtils.FilterOk(PaletteFilters.MatchesSubString, word, wordToMatchAgainst, expectedMatches);
}

[InlineData("aaaaaaaaaaaaaaaaaaaax", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")]
[Theory]
public void MatchesSubString_NotOk(string word, string wordToMatchAgainst)
{
FilterTestUtils.FilterNotOk(PaletteFilters.MatchesSubString, word, wordToMatchAgainst);
}
}
44 changes: 44 additions & 0 deletions src/Whim.CommandPalette.Tests/Filters/TestUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using Xunit;

namespace Whim.CommandPalette.Tests;

public static class FilterTestUtils
{
public static void FilterOk(PaletteFilter filter, string word, string wordToMatchAgainst, PaletteFilterTextMatch[]? expectedMatches = null)
{
PaletteFilterTextMatch[]? actualMatches = filter(word, wordToMatchAgainst);

if (expectedMatches == null)
{
Assert.Null(actualMatches);
return;
}

Assert.NotNull(actualMatches);
Assert.Equal(expectedMatches.Length, actualMatches!.Length);
for (int i = 0; i < expectedMatches.Length; i++)
{
PaletteFilterTextMatch expected = expectedMatches[i];
PaletteFilterTextMatch actual = actualMatches[i];

Assert.Equal(expected.Start, actual.Start);
Assert.Equal(expected.End, actual.End);
}
}

public static void FilterNotOk(PaletteFilter filter, string word, string wordToMatchAgainst)
{
FilterOk(filter, word, wordToMatchAgainst, null);
}

public static PaletteFilterTextMatch[] CreateExpectedMatches(params int[][] expected)
{
PaletteFilterTextMatch[] result = new PaletteFilterTextMatch[expected.Length];
for (int i = 0; i < expected.Length; i++)
{
int[] pair = expected[i];
result[i] = new PaletteFilterTextMatch(pair[0], pair[1]);
}
return result;
}
}
37 changes: 37 additions & 0 deletions src/Whim.CommandPalette.Tests/Filters/UtilsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Xunit;

namespace Whim.CommandPalette.Tests;

public class UtilsTests
{
private static PaletteFilter NewFilter(int[] counters, int i, bool r)
{
return (string word, string wordToMatchAgainst) =>
{
counters[i]++;
return r ? new[] { new PaletteFilterTextMatch(0, word.Length) } : null;
};
}

[InlineData(0, true, 1, false, new int[] { 1, 0 })]
[InlineData(0, true, 1, true, new int[] { 1, 0 })]
[InlineData(0, false, 1, true, new int[] { 1, 1 })]
[Theory]
public void Or(int i1, bool r1, int i2, bool r2, int[] expected)
{
int[] counters = new int[2];
PaletteFilter filter = PaletteFilters.Or(NewFilter(counters, i1, r1), NewFilter(counters, i2, r2));
FilterTestUtils.FilterOk(filter, "anything", "anything", new PaletteFilterTextMatch[] { new PaletteFilterTextMatch(0, 8) });
Assert.Equal(expected, counters);
}

[InlineData(0, false, 1, false, new int[] { 1, 1 })]
[Theory]
public void Or_NotOk(int i1, bool r1, int i2, bool r2, int[] expected)
{
int[] counters = new int[2];
PaletteFilter filter = PaletteFilters.Or(NewFilter(counters, i1, r1), NewFilter(counters, i2, r2));
FilterTestUtils.FilterNotOk(filter, "anything", "anything");
Assert.Equal(expected, counters);
}
}
67 changes: 67 additions & 0 deletions src/Whim.CommandPalette.Tests/Filters/WordsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using Xunit;

namespace Whim.CommandPalette.Tests;

public class WordsTests
{
[InlineData("alpha", "alpha", new int[] { 0, 5 })]
[InlineData("alpha", "alphasomething", new int[] { 0, 5 })]
[InlineData("a", "alpha", new int[] { 0, 1 })]
[InlineData("A", "alpha", new int[] { 0, 1 })]
[InlineData("AlPh", "alPHA", new int[] { 0, 4 })]
[InlineData("gp", "Git: Pull", new int[] { 0, 1 }, new int[] { 5, 6 })]
[InlineData("g p", "Git: Pull", new int[] { 0, 1 }, new int[] { 5, 6 })]
[InlineData("gipu", "Git: Pull", new int[] { 0, 2 }, new int[] { 5, 7 })]
[InlineData("gp", "Category: Git: Pull", new int[] { 10, 11 }, new int[] { 15, 16 })]
[InlineData("g p", "Category: Git: Pull", new int[] { 10, 11 }, new int[] { 15, 16 })]
[InlineData("gipu", "Category: Git: Pull", new int[] { 10, 12 }, new int[] { 15, 17 })]
[InlineData("git: プル", "git: プル", new int[] { 0, 7 })]
[InlineData("git プル", "git: プル", new int[] { 0, 3 }, new int[] { 5, 7 })]
[InlineData("öäk", "Öhm: Älles Klar", new int[] { 0, 1 }, new int[] { 5, 6 }, new int[] { 11, 12 })]
[InlineData("C++", "C/C++: command", new int[] { 2, 5 })]
[InlineData(".", ":")]
[InlineData(".", ".", new int[] { 0, 1 })]
[InlineData("bar", "foo-bar", new int[] { 4, 7 })]
[InlineData("bar test", "foo-bar test", new int[] { 4, 12 })]
[InlineData("fbt", "foo-bar test", new int[] { 0, 1 }, new int[] { 4, 5 }, new int[] { 8, 9 })]
[InlineData("bar test", "foo-bar (test)", new int[] { 4, 8 }, new int[] { 9, 13 })]
[InlineData("foo bar", "foo (bar)", new int[] { 0, 4 }, new int[] { 5, 8 })]
[InlineData("foo bar", "foo-bar", new int[] { 0, 3 }, new int[] { 4, 7 })]
[InlineData("foo bar", "123 foo-bar 456", new int[] { 4, 7 }, new int[] { 8, 11 })]
[InlineData("foo-bar", "foo bar", new int[] { 0, 3 }, new int[] { 4, 7 })]
[InlineData("foo:bar", "foo:bar", new int[] { 0, 7 })]
[Theory]
public void MatchesWordsSeparate_Ok(string query, string target, params int[][] expected)
{
PaletteFilterTextMatch[] expectedMatches = FilterTestUtils.CreateExpectedMatches(expected);
FilterTestUtils.FilterOk(PaletteFilters.MatchesWordsSeparate, query, target, expectedMatches);
}

[InlineData("alpha", "alp")]
[InlineData("x", "alpha")]
[InlineData("it", "Git: Pull")]
[InlineData("ll", "Git: Pull")]
[InlineData("bar est", "foo-bar test")]
[InlineData("fo ar", "foo-bar test")]
[InlineData("for", "foo-bar test")]
[Theory]
public void MatchesWordsSeparate_NotOk(string query, string target)
{
FilterTestUtils.FilterNotOk(PaletteFilters.MatchesWordsSeparate, query, target);
}

[InlineData("pu", "Category: Git: Pull", new int[] { 15, 17 })]
[Theory]
public void MatchesWordsContiguous_Ok(string query, string target, params int[][] expected)
{
PaletteFilterTextMatch[] expectedMatches = FilterTestUtils.CreateExpectedMatches(expected);
FilterTestUtils.FilterOk(PaletteFilters.MatchesWordsContiguous, query, target, expectedMatches);
}

[InlineData("gipu", "Category: Git: Pull")]
[Theory]
public void MatchesWordsContiguous_NotOk(string query, string target)
{
FilterTestUtils.FilterNotOk(PaletteFilters.MatchesWordsContiguous, query, target);
}
}
12 changes: 2 additions & 10 deletions src/Whim.CommandPalette.Tests/MatchTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Moq;
using Windows.Win32.UI.Input.KeyboardAndMouse;
using Xunit;

namespace Whim.CommandPalette.Tests;
Expand All @@ -9,15 +8,8 @@ public class MatchTests
[Fact]
public void Match_NoKeybind()
{
Match match = new(new Mock<ICommand>().Object);
CommandItem match = new(new Mock<ICommand>().Object);

Assert.Null(match.Keys);
}

[Fact]
public void Match_Keybind()
{
Match match = new(new Mock<ICommand>().Object, new Keybind(KeyModifiers.LWin, VIRTUAL_KEY.VK_A));
Assert.Equal("LWin + A", match.Keys);
Assert.Null(match.Keybind);
}
}
Loading

0 comments on commit c4c8f4b

Please sign in to comment.