Skip to content

Commit ea27622

Browse files
authored
Version 1.0.3 - Merge pull request #10 from Odotocodot/dev
### Changes - Check for valid path to vswhere.exe - Add new setting for custom path to vswhere.exe - Force result scoring to match Visual Studio. Closes #11. (this fix will revert back to Flow Launcher ordering if you select the same result over [10,000](https://github.com/Odotocodot/VisualStudio4Flow/blob/a8b788ebeb3d8910f262f4eb03a0df5493687fe1/Main.cs#L22) times) - Updated search functionality to be more "fuzzy" and results ordered based on match score.
2 parents c76d064 + 0b01a31 commit ea27622

9 files changed

Lines changed: 157 additions & 65 deletions

Main.cs

Lines changed: 59 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace Flow.Launcher.Plugin.VisualStudio
99
{
1010
public class Main : IAsyncPlugin, IContextMenu, ISettingProvider, IAsyncReloadable
1111
{
12-
public PluginInitContext context;
12+
private PluginInitContext context;
1313
private VisualStudioPlugin plugin;
1414

1515
private static readonly TypeKeyword ProjectsOnly = new(0, "p:");
@@ -19,6 +19,8 @@ public class Main : IAsyncPlugin, IContextMenu, ISettingProvider, IAsyncReloadab
1919
private IconProvider iconProvider;
2020
private Dictionary<Entry, List<int>> entryHighlightData;
2121

22+
private const int ScoreIncrement = 10000;
23+
2224
public async Task InitAsync(PluginInitContext context)
2325
{
2426
this.context = context;
@@ -54,34 +56,49 @@ public async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
5456
return null;
5557
}
5658

59+
if (!plugin.ValidVswherePath)
60+
{
61+
return SingleResult("Could not find vswhere.exe. Please set the path in the plugin settings.", context.API.OpenSettingDialog);
62+
}
63+
5764
if (!plugin.IsVSInstalled)
5865
{
5966
return SingleResult("No installed version of Visual Studio was found");
6067
}
6168

62-
if(query.IsReQuery)
69+
if (query.IsReQuery)
6370
{
6471
await plugin.GetRecentEntries(token);
65-
}
72+
}
6673

6774
if (!plugin.RecentEntries.Any())
6875
{
6976
return SingleResult("No recent items found");
7077
}
7178

7279
entryHighlightData.Clear();
73-
var selectedRecentItems = query.Search switch
80+
81+
if (string.IsNullOrWhiteSpace(query.Search))
7482
{
75-
string search when string.IsNullOrEmpty(search) => plugin.RecentEntries,
76-
string search when search.StartsWith(ProjectsOnly.Keyword) => plugin.RecentEntries.Where(e => TypeSearch(e, query, ProjectsOnly)),
77-
string search when search.StartsWith(FilesOnly.Keyword) => plugin.RecentEntries.Where(e => TypeSearch(e, query, FilesOnly)),
78-
_ => plugin.RecentEntries.Where(e => FuzzySearch(e, query.Search))
83+
return plugin.RecentEntries.OrderBy(e => e.Value.LastAccessed)
84+
.Select((e, i) => CreateEntryResult(e, i * ScoreIncrement))
85+
.ToList();
86+
}
87+
88+
Func<EntryScore, Query, bool> searchFunc = query.Search switch
89+
{
90+
string search when search.StartsWith(ProjectsOnly.Keyword) => (x, query) => TypeSearch(x, query, ProjectsOnly),
91+
string search when search.StartsWith(FilesOnly.Keyword) => (x, query) => TypeSearch(x, query, FilesOnly),
92+
_ => (x, query) => FuzzySearch(x, query.Search),
7993
};
8094

81-
return selectedRecentItems.OrderBy(e => e.Value.LastAccessed)
82-
.Select(CreateEntryResult)
83-
.ToList();
95+
return plugin.RecentEntries.Select(x => new EntryScore(x))
96+
.Where(x => searchFunc(x, query))
97+
.Select(x => CreateEntryResult(x.Entry, x.Score))
98+
.ToList();
8499
}
100+
101+
85102
public List<Result> LoadContextMenus(Result selectedResult)
86103
{
87104
if (selectedResult.ContextData is Entry currentEntry)
@@ -93,7 +110,7 @@ public List<Result> LoadContextMenus(Result selectedResult)
93110
Title = $"Open in \"{vs.DisplayName}\" [Version: {vs.DisplayVersion}]",
94111
SubTitle = vs.ExePath,
95112
IcoPath = iconProvider.GetIconPath(vs),
96-
Action = c =>
113+
Action = _ =>
97114
{
98115
context.API.ShellRun($"\"{currentEntry.Path}\"", $"\"{vs.ExePath}\"");
99116
return true;
@@ -104,20 +121,20 @@ public List<Result> LoadContextMenus(Result selectedResult)
104121
Title = $"Remove \"{selectedResult.Title}\" from recent items list.",
105122
SubTitle = selectedResult.SubTitle,
106123
IcoPath = IconProvider.Remove,
107-
AsyncAction = async c =>
124+
AsyncAction = async _ =>
108125
{
109126
await plugin.RemoveEntry(currentEntry);
110127
await Task.Delay(100);
111-
112-
context.API.ChangeQuery(context.CurrentPluginMetadata.ActionKeyword, false);
128+
129+
context.API.ChangeQuery(context.CurrentPluginMetadata.ActionKeyword);
113130
return true;
114131
}
115132
}).Append(new Result
116133
{
117134
Title = $"Open in File Explorer",
118135
SubTitle = currentEntry.Path,
119136
IcoPath = IconProvider.Folder,
120-
Action = c =>
137+
Action = _ =>
121138
{
122139
context.API.OpenDirectory(Path.GetDirectoryName(currentEntry.Path), currentEntry.Path);
123140
return true;
@@ -128,20 +145,25 @@ public List<Result> LoadContextMenus(Result selectedResult)
128145
return null;
129146
}
130147

131-
private static List<Result> SingleResult(string title)
148+
private static List<Result> SingleResult(string title, Action action = null)
132149
{
133150
return new List<Result>
134151
{
135152
new Result
136153
{
137154
Title = title,
138155
IcoPath = IconProvider.DefaultIcon,
156+
Action = _ =>
157+
{
158+
action?.Invoke();
159+
return action != null;
160+
}
139161
}
140162
};
141163
}
142-
private Result CreateEntryResult(Entry e)
164+
165+
private Result CreateEntryResult(Entry e, int score)
143166
{
144-
string iconPath = IconProvider.DefaultIcon;
145167
Action action = () => context.API.ShellRun($"\"{e.Path}\"");
146168
if (!string.IsNullOrWhiteSpace(settings.DefaultVSId))
147169
{
@@ -160,37 +182,48 @@ private Result CreateEntryResult(Entry e)
160182
SubTitle = e.Value.IsFavorite ? $"★ {e.Path}" : e.Path,
161183
SubTitleToolTip = $"{e.Path}\n\nLast Accessed:\t{e.Value.LastAccessed:F}",
162184
ContextData = e,
163-
IcoPath = iconPath,
164-
Action = c =>
185+
Score = score,
186+
IcoPath = IconProvider.DefaultIcon,
187+
Action = _ =>
165188
{
166189
action();
167190
return true;
168191
}
169192
};
170193
}
171194

172-
private bool FuzzySearch(Entry entry, string search)
195+
private bool FuzzySearch(EntryScore entryScore, string search)
173196
{
197+
var entry = entryScore.Entry;
174198
var matchResult = context.API.FuzzySearch(search, Path.GetFileNameWithoutExtension(entry.Path));
175199
entryHighlightData[entry] = matchResult.MatchData;
176-
return matchResult.IsSearchPrecisionScoreMet();
200+
entryScore.Score = matchResult.Score;
201+
return matchResult.Success;
177202
}
178-
private bool TypeSearch(Entry entry, Query query, TypeKeyword typeKeyword)
203+
204+
private bool TypeSearch(EntryScore entryScore, Query query, TypeKeyword typeKeyword)
179205
{
206+
var entry = entryScore.Entry;
180207
var search = query.Search[typeKeyword.Keyword.Length..];
181208
if (string.IsNullOrWhiteSpace(search))
182209
{
183210
return entry.ItemType == typeKeyword.Type;
184211
}
185212
else
186213
{
187-
return entry.ItemType == typeKeyword.Type && FuzzySearch(entry, search);
214+
return entry.ItemType == typeKeyword.Type && FuzzySearch(entryScore, search);
188215
}
189216
}
217+
190218
public System.Windows.Controls.Control CreateSettingPanel()
191219
{
192220
return new UI.SettingsView(new UI.SettingsViewModel(settings, plugin, iconProvider, this));
193221
}
194-
public record struct TypeKeyword(int Type, string Keyword);
222+
223+
private record struct TypeKeyword(int Type, string Keyword);
224+
private record EntryScore(Entry Entry)
225+
{
226+
public int Score { get; set; } = 0;
227+
}
195228
}
196229
}

Readme.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Search all your recent items from **all** your Visual Studio installations.
3838
3939
### Context Menu
4040

41-
Press <kbd>⇧ Shift</kbd> + <kbd>⇥ Tab</kbd> or right-click on a search result (a recent item) to open the context menu shown below.
41+
Use the "Open Context Menu" hotkey in your Flow Launcher settings (default is <kbd>⇧ Shift</kbd> + <kbd>⏎ Enter</kbd> or right-click) on a search result (a recent item) to open the context menu shown below.
4242

4343
This allows for:
4444
- Opening the item in a specific installation of Visual Studio.
@@ -62,6 +62,8 @@ This allows for:
6262
1. Restore recent items to the current backup
6363
2. Backup recent items right now
6464
3. Stop automatically updating the backup
65+
6. The location of [vswhere.exe](https://github.com/microsoft/vswhere). The default value, set automatically by the plugin is vswhere's default install location. \
66+
However, if this is incorrect you can change it! Though, you will need to reload plugin data (press <kbd>F5</kbd> when the Flow Launcher search window is open) or restart Flow Launcher for the change to take effect.
6567

6668
## Known Issues
6769

Settings.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.IO;
23

34
namespace Flow.Launcher.Plugin.VisualStudio
45
{
@@ -9,5 +10,9 @@ public class Settings
910
public bool AutoUpdateBackup { get; set; } = true;
1011
public DateTime LastBackup { get; set; } = DateTime.MinValue;
1112
public Entry[] EntriesBackup { get; set; }
13+
public string VswherePath { get; set; } = DefaultVswherePath;
14+
15+
public static string DefaultVswherePath { get; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
16+
"Microsoft Visual Studio\\Installer\\vswhere.exe");
1217
}
1318
}

UI/SettingsView.xaml

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,12 @@
2020
<Border CornerRadius="0,0,0,0" Style="{DynamicResource SettingGroupBox}">
2121
<ItemsControl Style="{StaticResource SettingGrid}">
2222
<StackPanel Style="{StaticResource TextPanel}">
23-
<TextBlock Style="{DynamicResource SettingTitleLabel}" Text="Default Open With" />
23+
<TextBlock Style="{DynamicResource SettingTitleLabel}" Text="Default open with" />
2424
<TextBlock Style="{DynamicResource SettingSubTitleLabel}" Text="Set the default Visual Studio instance to open a search result with." />
2525
</StackPanel>
2626
<StackPanel Grid.Column="2" Orientation="Horizontal">
2727
<ComboBox
2828
Name="ComboBox"
29-
Grid.Column="2"
3029
MinWidth="350"
3130
Margin="10,4,12,4"
3231
d:SelectedIndex="0"
@@ -64,7 +63,7 @@
6463
<TextBlock Style="{StaticResource Glyph}" Text="&#xe7ac;" />
6564
</ItemsControl>
6665
</Border>
67-
<Border CornerRadius="0,0,9,9" Style="{DynamicResource SettingGroupBox}">
66+
<Border CornerRadius="0,0,0,0" Style="{DynamicResource SettingGroupBox}">
6867
<ItemsControl Style="{StaticResource SettingGrid}">
6968
<StackPanel Style="{StaticResource TextPanel}">
7069
<TextBlock Style="{DynamicResource SettingTitleLabel}" Text="Remove recent items from Visual Studio" />
@@ -124,5 +123,17 @@
124123
<TextBlock Style="{StaticResource Glyph}" Text="&#xe74d;" />
125124
</ItemsControl>
126125
</Border>
126+
<Border CornerRadius="0,0,9,9" Style="{DynamicResource SettingGroupBox}">
127+
<ItemsControl Style="{StaticResource SettingGrid}">
128+
<StackPanel Style="{StaticResource TextPanel}">
129+
<TextBlock Style="{DynamicResource SettingTitleLabel}" Text="vswhere.exe path" ToolTip="{Binding DefaultVswherePath}" />
130+
<TextBlock Style="{DynamicResource SettingSubTitleLabel}" Text="vswhere.exe is used to find your Visual Studio instances." />
131+
</StackPanel>
132+
<StackPanel Grid.Column="2" Orientation="Horizontal">
133+
<TextBox Margin="0,4,18,4" Text="{Binding VswherePath}" />
134+
</StackPanel>
135+
<TextBlock Style="{StaticResource Glyph}" Text="&#xe835;" />
136+
</ItemsControl>
137+
</Border>
127138
</StackPanel>
128139
</UserControl>

UI/SettingsViewModel.cs

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,26 @@ public SettingsViewModel(Settings settings, VisualStudioPlugin plugin, IconProvi
2525
public List<VisualStudioModel> VSInstances { get; set; }
2626
public VisualStudioModel SelectedVSInstance
2727
{
28-
get => selectedVSInstance;
28+
get => selectedVSInstance;
2929
set
3030
{
3131
selectedVSInstance = value;
3232
settings.DefaultVSId = selectedVSInstance.InstanceId;
3333
OnPropertyChanged();
3434
}
3535
}
36+
37+
public string VswherePath
38+
{
39+
get => settings.VswherePath;
40+
set
41+
{
42+
settings.VswherePath = value;
43+
OnPropertyChanged();
44+
}
45+
}
46+
47+
public string DefaultVswherePath => $"Default Path: \"{Settings.DefaultVswherePath}\"";
3648
public string LastBackup => $"[Last Backup: {settings.LastBackup.ToLocalTime()}]";
3749
public bool AutoUpdateBackup
3850
{
@@ -46,7 +58,17 @@ public bool AutoUpdateBackup
4658

4759
private void SetupVSInstances(Settings settings, VisualStudioPlugin plugin)
4860
{
49-
VSInstances = new List<VisualStudioModel>(plugin.VSInstances.Select(vs =>
61+
VSInstances = new List<VisualStudioModel>()
62+
{
63+
new VisualStudioModel
64+
{
65+
IconPath = iconProvider.Windows,
66+
Name = "Let Windows Decide (Default)",
67+
InstanceId = null,
68+
}
69+
};
70+
71+
VSInstances.AddRange(plugin.VSInstances.Select(vs =>
5072
{
5173
return new VisualStudioModel
5274
{
@@ -55,13 +77,8 @@ private void SetupVSInstances(Settings settings, VisualStudioPlugin plugin)
5577
InstanceId = vs.InstanceId,
5678
};
5779
}));
58-
VSInstances.Insert(0, new VisualStudioModel
59-
{
60-
IconPath = iconProvider.Windows,
61-
Name = "Let Windows Decide (Default)",
62-
InstanceId = null,
63-
});
64-
SelectedVSInstance = VSInstances.FirstOrDefault(i => i.InstanceId == settings.DefaultVSId);
80+
81+
SelectedVSInstance = VSInstances.FirstOrDefault(i => i.InstanceId == settings.DefaultVSId, VSInstances[0]);
6582
}
6683
public async Task RefreshInstances()
6784
{
@@ -75,11 +92,6 @@ public async Task RefreshInstances()
7592
public async Task BackupNow() => await Task.Run(plugin.UpdateBackup);
7693
public void UpdateLastBackupTime() => OnPropertyChanged(nameof(LastBackup));
7794

78-
public class VisualStudioModel
79-
{
80-
public string IconPath { get; init; }
81-
public string Name { get; init; }
82-
public string InstanceId { get; init; }
83-
}
95+
8496
}
8597
}

UI/VisualStudioModel.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace Flow.Launcher.Plugin.VisualStudio.UI;
2+
3+
public class VisualStudioModel
4+
{
5+
public string IconPath { get; init; }
6+
public string Name { get; init; }
7+
public string InstanceId { get; init; }
8+
}

0 commit comments

Comments
 (0)