diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 9b07f6f20b..e34159b80c 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -205,6 +205,7 @@ coclass codereview Codespaces COINIT +colid colorconv colorformat colorhistory @@ -279,8 +280,12 @@ datareader datatracker dataversion Dayof +DBID DBLCLKS DBLEPSILON +DBPROP +DBPROPIDSET +DBPROPSET DCapture DCBA DCOM @@ -490,6 +495,7 @@ flac flaticon flyouts FMask +fmtid FOF FOFX FOLDERID @@ -611,6 +617,7 @@ HREDRAW hres hresult hrgn +HROW hsb HSCROLL hsi @@ -914,6 +921,8 @@ mscorlib msdata MSDL MSGFLT +MSIDXS +MSIDXSPROP msiexec MSIFASTINSTALL MSIHANDLE @@ -1057,6 +1066,7 @@ oldpath oldtheme oleaut OLECHAR +openas opencode OPENFILENAME opensource @@ -1183,6 +1193,8 @@ previouscamera PREVIOUSINSTALLFOLDER PREVIOUSVERSIONSINSTALLED prevpane +prg +prgh prgms pri PRINTCLIENT @@ -1199,6 +1211,8 @@ programdata projectname PROPBAG PROPERTYKEY +propkey +Propset PROPVARIANT prvpane psapi @@ -1310,6 +1324,7 @@ RGBQUAD rgbs rgelt rgf +rgh rgn rgs RIDEV @@ -1320,6 +1335,7 @@ RKey RNumber rop ROUNDSMALL +ROWSETEXT rpcrt RRF rrr @@ -1361,6 +1377,7 @@ searchterm searchtext SEARCHUI secpol +SEEMASKINVOKEIDLIST SENDCHANGE sendvirtualinput seperators @@ -1664,6 +1681,7 @@ uwp vabdq validmodulename valuegenerator +VARENUM variantassignment vcamp vcdl @@ -1744,6 +1762,7 @@ webpage websites wekyb wgpocpl +WHEREID Wholegrain wic wifi diff --git a/PowerToys.sln b/PowerToys.sln index 0981d156fc..00ab0d1eb1 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -707,6 +707,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Project Templates", "Projec EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "NewPlus.ShellExtension.win10", "src\modules\NewPlus\NewShellExtensionContextMenu.win10\NewPlus.ShellExtension.win10.vcxproj", "{0DB0F63A-D2F8-4DA3-A650-2D0B8724218E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Indexer", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.Indexer\Microsoft.CmdPal.Ext.Indexer.csproj", "{453CBB73-A3CB-4D0B-8D24-6940B86FE21D}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Shell", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.Shell\Microsoft.CmdPal.Ext.Shell.csproj", "{C0CE3B5E-16D3-495D-B335-CA791B660162}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.WindowWalker", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.WindowWalker\Microsoft.CmdPal.Ext.WindowWalker.csproj", "{3A9A7297-92C4-4F16-B6F9-8D4AB652C86C}" @@ -3359,6 +3361,24 @@ Global {D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}.Release|x64.Build.0 = Release|x64 {D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}.Release|x86.ActiveCfg = Release|x64 {D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}.Release|x86.Build.0 = Release|x64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Debug|ARM64.Build.0 = Debug|ARM64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Debug|ARM64.Deploy.0 = Debug|ARM64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Debug|x64.ActiveCfg = Debug|x64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Debug|x64.Build.0 = Debug|x64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Debug|x64.Deploy.0 = Debug|x64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Debug|x86.ActiveCfg = Debug|x64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Debug|x86.Build.0 = Debug|x64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Debug|x86.Deploy.0 = Debug|x64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Release|ARM64.ActiveCfg = Release|ARM64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Release|ARM64.Build.0 = Release|ARM64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Release|ARM64.Deploy.0 = Release|ARM64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Release|x64.ActiveCfg = Release|x64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Release|x64.Build.0 = Release|x64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Release|x64.Deploy.0 = Release|x64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Release|x86.ActiveCfg = Release|x64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Release|x86.Build.0 = Release|x64 + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Release|x86.Deploy.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -3627,6 +3647,7 @@ Global {B79B52FB-8B2E-4CF5-B0FE-37E3E981AC7A} = {071E18A4-A530-46B8-AB7D-B862EE55E24E} {89D0E199-B17A-418C-B2F8-7375B6708357} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF} {C0CE3B5E-16D3-495D-B335-CA791B660162} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} + {453CBB73-A3CB-4D0B-8D24-6940B86FE21D} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {3846508C-77EB-4034-A702-F8BB263C4F79} {0DB0F63A-D2F8-4DA3-A650-2D0B8724218E} = {CA716AE6-FE5C-40AC-BB8F-2C87912687AC} {3A9A7297-92C4-4F16-B6F9-8D4AB652C86C} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/CopyPathCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/CopyPathCommand.cs new file mode 100644 index 0000000000..77ccccf376 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/CopyPathCommand.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CmdPal.Ext.Indexer.Data; +using Microsoft.CmdPal.Ext.Indexer.Properties; +using Microsoft.CmdPal.Extensions.Helpers; +using Windows.ApplicationModel.DataTransfer; + +namespace Microsoft.CmdPal.Ext.Indexer.Commands; + +internal sealed partial class CopyPathCommand : InvokableCommand +{ + private readonly IndexerItem _item; + + internal CopyPathCommand(IndexerItem item) + { + this._item = item; + this.Name = Resources.Indexer_Command_CopyPath; + this.Icon = new("\uE8c8"); + } + + public override CommandResult Invoke() + { + try + { + var dataPackage = new DataPackage(); + dataPackage.SetText(_item.FullPath); + Clipboard.SetContent(dataPackage); + Clipboard.Flush(); + } + catch + { + } + + return CommandResult.KeepOpen(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/OpenFileCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/OpenFileCommand.cs new file mode 100644 index 0000000000..b8b372767e --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/OpenFileCommand.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.ComponentModel; +using System.Diagnostics; +using ManagedCommon; +using Microsoft.CmdPal.Ext.Indexer.Data; +using Microsoft.CmdPal.Ext.Indexer.Properties; +using Microsoft.CmdPal.Extensions.Helpers; + +namespace Microsoft.CmdPal.Ext.Indexer.Commands; + +internal sealed partial class OpenFileCommand : InvokableCommand +{ + private readonly IndexerItem _item; + + internal OpenFileCommand(IndexerItem item) + { + this._item = item; + this.Name = Resources.Indexer_Command_OpenFile; + this.Icon = new("\uE8E5"); + } + + public override CommandResult Invoke() + { + using (var process = new Process()) + { + process.StartInfo.FileName = _item.FullPath; + process.StartInfo.UseShellExecute = true; + + try + { + process.Start(); + } + catch (Win32Exception ex) + { + Logger.LogError($"Unable to open {_item.FullPath}", ex); + } + } + + return CommandResult.GoHome(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/OpenInConsoleCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/OpenInConsoleCommand.cs new file mode 100644 index 0000000000..dbd6dfd948 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/OpenInConsoleCommand.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using ManagedCommon; +using Microsoft.CmdPal.Ext.Indexer.Data; +using Microsoft.CmdPal.Ext.Indexer.Properties; +using Microsoft.CmdPal.Extensions.Helpers; + +namespace Microsoft.CmdPal.Ext.Indexer.Commands; + +internal sealed partial class OpenInConsoleCommand : InvokableCommand +{ + private readonly IndexerItem _item; + + internal OpenInConsoleCommand(IndexerItem item) + { + this._item = item; + this.Name = Resources.Indexer_Command_OpenPathInConsole; + this.Icon = new("\uE756"); + } + + public override CommandResult Invoke() + { + using (var process = new Process()) + { + process.StartInfo.WorkingDirectory = Path.GetDirectoryName(_item.FullPath); + process.StartInfo.FileName = "cmd.exe"; + + try + { + process.Start(); + } + catch (Win32Exception ex) + { + Logger.LogError($"Unable to open {_item.FullPath}", ex); + } + } + + return CommandResult.GoHome(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/OpenPropertiesCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/OpenPropertiesCommand.cs new file mode 100644 index 0000000000..5bd1cc625a --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/OpenPropertiesCommand.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; +using ManagedCommon; +using Microsoft.CmdPal.Ext.Indexer.Data; +using Microsoft.CmdPal.Ext.Indexer.Native; +using Microsoft.CmdPal.Ext.Indexer.Properties; +using Microsoft.CmdPal.Extensions.Helpers; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.Shell; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace Microsoft.CmdPal.Ext.Indexer.Commands; + +internal sealed partial class OpenPropertiesCommand : InvokableCommand +{ + private readonly IndexerItem _item; + + private static unsafe bool ShowFileProperties(string filename) + { + var propertiesPtr = Marshal.StringToHGlobalUni("properties"); + var filenamePtr = Marshal.StringToHGlobalUni(filename); + + try + { + var filenamePCWSTR = new PCWSTR((char*)filenamePtr); + var propertiesPCWSTR = new PCWSTR((char*)propertiesPtr); + + var info = new SHELLEXECUTEINFOW + { + cbSize = (uint)Marshal.SizeOf(), + lpVerb = propertiesPCWSTR, + lpFile = filenamePCWSTR, + nShow = (int)SHOW_WINDOW_CMD.SW_SHOW, + fMask = NativeHelpers.SEEMASKINVOKEIDLIST, + }; + + return PInvoke.ShellExecuteEx(ref info); + } + finally + { + Marshal.FreeHGlobal(filenamePtr); + Marshal.FreeHGlobal(propertiesPtr); + } + } + + internal OpenPropertiesCommand(IndexerItem item) + { + this._item = item; + this.Name = Resources.Indexer_Command_OpenProperties; + this.Icon = new("\uE90F"); + } + + public override CommandResult Invoke() + { + try + { + ShowFileProperties(_item.FullPath); + } + catch (Exception ex) + { + Logger.LogError("Error showing file properties: ", ex); + } + + return CommandResult.GoHome(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/OpenWithCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/OpenWithCommand.cs new file mode 100644 index 0000000000..3f9defeef9 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/OpenWithCommand.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.InteropServices; +using Microsoft.CmdPal.Ext.Indexer.Data; +using Microsoft.CmdPal.Ext.Indexer.Native; +using Microsoft.CmdPal.Ext.Indexer.Properties; +using Microsoft.CmdPal.Extensions.Helpers; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.Shell; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace Microsoft.CmdPal.Ext.Indexer.Commands; + +internal sealed partial class OpenWithCommand : InvokableCommand +{ + private readonly IndexerItem _item; + + private static unsafe bool OpenWith(string filename) + { + var filenamePtr = Marshal.StringToHGlobalUni(filename); + var verbPtr = Marshal.StringToHGlobalUni("openas"); + + try + { + var filenamePCWSTR = new PCWSTR((char*)filenamePtr); + var verbPCWSTR = new PCWSTR((char*)verbPtr); + + var info = new SHELLEXECUTEINFOW + { + cbSize = (uint)Marshal.SizeOf(), + lpVerb = verbPCWSTR, + lpFile = filenamePCWSTR, + nShow = (int)SHOW_WINDOW_CMD.SW_SHOWNORMAL, + fMask = NativeHelpers.SEEMASKINVOKEIDLIST, + }; + + return PInvoke.ShellExecuteEx(ref info); + } + finally + { + Marshal.FreeHGlobal(filenamePtr); + Marshal.FreeHGlobal(verbPtr); + } + } + + internal OpenWithCommand(IndexerItem item) + { + this._item = item; + this.Name = Resources.Indexer_Command_OpenWith; + this.Icon = new("\uE7AC"); + } + + public override CommandResult Invoke() + { + OpenWith(_item.FullPath); + + return CommandResult.GoHome(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/ShowFileInFolderCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/ShowFileInFolderCommand.cs new file mode 100644 index 0000000000..9b60c9b1c9 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/ShowFileInFolderCommand.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; +using System.IO; +using ManagedCommon; +using Microsoft.CmdPal.Ext.Indexer.Data; +using Microsoft.CmdPal.Ext.Indexer.Properties; +using Microsoft.CmdPal.Extensions.Helpers; + +namespace Microsoft.CmdPal.Ext.Indexer.Commands; + +internal sealed partial class ShowFileInFolderCommand : InvokableCommand +{ + private readonly IndexerItem _item; + + internal ShowFileInFolderCommand(IndexerItem item) + { + this._item = item; + this.Name = Resources.Indexer_Command_ShowInFolder; + this.Icon = new("\uE838"); + } + + public override CommandResult Invoke() + { + if (File.Exists(_item.FullPath)) + { + try + { + var argument = "/select, \"" + _item.FullPath + "\""; + Process.Start("explorer.exe", argument); + } + catch (Exception ex) + { + Logger.LogError("Invoke exception: ", ex); + } + } + + return CommandResult.GoHome(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Data/IndexerItem.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Data/IndexerItem.cs new file mode 100644 index 0000000000..f415ea4074 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Data/IndexerItem.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.Ext.Indexer.Data; + +internal sealed class IndexerItem +{ + internal string FullPath { get; init; } + + internal string FileName { get; init; } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Data/IndexerListItem.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Data/IndexerListItem.cs new file mode 100644 index 0000000000..3be93172eb --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Data/IndexerListItem.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CmdPal.Ext.Indexer.Commands; +using Microsoft.CmdPal.Extensions.Helpers; + +namespace Microsoft.CmdPal.Ext.Indexer.Data; + +internal sealed partial class IndexerListItem : ListItem +{ + private readonly IndexerItem _indexerItem; + + public IndexerListItem(IndexerItem indexerItem) + : base(new OpenFileCommand(indexerItem)) + { + _indexerItem = indexerItem; + Title = indexerItem.FileName; + Subtitle = indexerItem.FullPath; + + MoreCommands = [ + new CommandContextItem(new OpenWithCommand(indexerItem)), + new CommandContextItem(new ShowFileInFolderCommand(indexerItem)), + new CommandContextItem(new CopyPathCommand(indexerItem)), + new CommandContextItem(new OpenInConsoleCommand(indexerItem)), + new CommandContextItem(new OpenPropertiesCommand(indexerItem)), + ]; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/DataSourceManager.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/DataSourceManager.cs new file mode 100644 index 0000000000..e186251825 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/DataSourceManager.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using ManagedCommon; +using Windows.Win32; +using Windows.Win32.System.Com; +using Windows.Win32.System.Search; + +namespace Microsoft.CmdPal.Ext.Indexer.Indexer; + +internal static class DataSourceManager +{ + private static readonly Guid CLSIDCollatorDataSource = new("9E175B8B-F52A-11D8-B9A5-505054503030"); + + private static IDBInitialize _dataSource; + + public static IDBInitialize GetDataSource() + { + if (_dataSource == null) + { + InitializeDataSource(); + } + + return _dataSource; + } + + private static bool InitializeDataSource() + { + var hr = PInvoke.CoCreateInstance(CLSIDCollatorDataSource, null, CLSCTX.CLSCTX_INPROC_SERVER, typeof(IDBInitialize).GUID, out var dataSourceObj); + if (hr != 0) + { + Logger.LogError("CoCreateInstance failed: " + hr); + return false; + } + + if (dataSourceObj == null) + { + Logger.LogError("CoCreateInstance failed: dataSourceObj is null"); + return false; + } + + _dataSource = (IDBInitialize)dataSourceObj; + _dataSource.Initialize(); + + return true; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/DBPROP.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/DBPROP.cs new file mode 100644 index 0000000000..d4be1b967f --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/DBPROP.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.InteropServices; +using Windows.Win32.Storage.IndexServer; +using Windows.Win32.System.Com.StructuredStorage; + +namespace Microsoft.CmdPal.Ext.Indexer.Indexer.OleDB; + +[StructLayout(LayoutKind.Sequential)] +internal struct DBPROP +{ +#pragma warning disable SA1307 // Accessible fields should begin with upper-case letter + public uint dwPropertyID; + public uint dwOptions; + public uint dwStatus; + public DBID colid; + public PROPVARIANT vValue; +#pragma warning restore SA1307 // Accessible fields should begin with upper-case letter +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/DBPROPIDSET.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/DBPROPIDSET.cs new file mode 100644 index 0000000000..9226e76757 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/DBPROPIDSET.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.CmdPal.Ext.Indexer.Indexer.OleDB; + +[StructLayout(LayoutKind.Sequential)] +public struct DBPROPIDSET +{ +#pragma warning disable SA1307 // Accessible fields should begin with upper-case letter + public IntPtr rgPropertyIDs; // Pointer to array of property IDs + public uint cPropertyIDs; // Number of properties in array + public Guid guidPropertySet; // GUID of the property set +#pragma warning restore SA1307 // Accessible fields should begin with upper-case letter +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/DBPROPSET.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/DBPROPSET.cs new file mode 100644 index 0000000000..7b38b9debe --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/DBPROPSET.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.CmdPal.Ext.Indexer.Indexer.OleDB; + +[StructLayout(LayoutKind.Sequential)] +public struct DBPROPSET +{ +#pragma warning disable SA1307 // Accessible fields should begin with upper-case letter + public IntPtr rgProperties; // Pointer to an array of DBPROP + public uint cProperties; // Number of properties in the array + public Guid guidPropertySet; // GUID of the property set +#pragma warning restore SA1307 // Accessible fields should begin with upper-case letter +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/IRowset.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/IRowset.cs new file mode 100644 index 0000000000..3127e2e563 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/IRowset.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.CmdPal.Ext.Indexer.Indexer.OleDB; + +[ComImport] +[Guid("0c733a7c-2a1c-11ce-ade5-00aa0044773d")] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +public interface IRowset +{ + [PreserveSig] + int AddRefRows( + uint cRows, + [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] IntPtr[] rghRows, + [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] uint[] rgRefCounts, + [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] int[] rgRowStatus); + + [PreserveSig] + int GetData( + IntPtr hRow, + IntPtr hAccessor, + IntPtr pData); + + [PreserveSig] + int GetNextRows( + IntPtr hReserved, + long lRowsOffset, + long cRows, + out uint pcRowsObtained, + out IntPtr prghRows); + + [PreserveSig] + int ReleaseRows( + uint cRows, + [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] IntPtr[] rghRows, + IntPtr rgRowOptions, + [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] uint[] rgRefCounts, + [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] int[] rgRowStatus); +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/IRowsetInfo.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/IRowsetInfo.cs new file mode 100644 index 0000000000..5c891c8036 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/OleDB/IRowsetInfo.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.CmdPal.Ext.Indexer.Indexer.OleDB; + +[ComImport] +[Guid("0C733A55-2A1C-11CE-ADE5-00AA0044773D")] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +public interface IRowsetInfo +{ + [PreserveSig] + int GetProperties( + uint cPropertyIDSets, + [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] DBPROPIDSET[] rgPropertyIDSets, + out ulong pcPropertySets, + out IntPtr prgPropertySets); + + [PreserveSig] + int GetReferencedRowset( + uint iOrdinal, + [In] ref Guid riid, + [Out, MarshalAs(UnmanagedType.Interface)] out object ppReferencedRowset); + + [PreserveSig] + int GetSpecification( + [In] ref Guid riid, + [Out, MarshalAs(UnmanagedType.Interface)] out object ppSpecification); +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/SearchResult.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/SearchResult.cs new file mode 100644 index 0000000000..1840338b73 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/SearchResult.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using ManagedCommon; +using Microsoft.CmdPal.Ext.Indexer.Indexer.Utils; +using Microsoft.CmdPal.Ext.Indexer.Native; +using Windows.Win32.System.Com; +using Windows.Win32.System.Com.StructuredStorage; +using Windows.Win32.UI.Shell.PropertiesSystem; + +namespace Microsoft.CmdPal.Ext.Indexer.Indexer; + +internal sealed class SearchResult +{ + public string ItemDisplayName { get; init; } + + public string ItemUrl { get; init; } + + public string LaunchUri { get; init; } + + public bool IsFolder { get; init; } + + public SearchResult(string name, string url, string filePath, bool isFolder) + { + ItemDisplayName = name; + ItemUrl = url; + IsFolder = isFolder; + + if (LaunchUri == null || LaunchUri.Length == 0) + { + // Launch the file with the default app, so use the file path + LaunchUri = filePath; + } + } + + public static unsafe SearchResult Create(IPropertyStore propStore) + { + try + { + var key = NativeHelpers.PropertyKeys.PKEYItemNameDisplay; + propStore.GetValue(&key, out var itemNameDisplay); + + key = NativeHelpers.PropertyKeys.PKEYItemUrl; + propStore.GetValue(&key, out var itemUrl); + + key = NativeHelpers.PropertyKeys.PKEYKindText; + propStore.GetValue(&key, out var kindText); + + var filePath = GetFilePath(ref itemUrl); + var isFolder = IsFoder(ref kindText); + + // Create the actual result object + var searchResult = new SearchResult( + GetStringFromPropVariant(ref itemNameDisplay), + GetStringFromPropVariant(ref itemUrl), + filePath, + isFolder); + + return searchResult; + } + catch (Exception ex) + { + Logger.LogError("Failed to get property values from propStore.", ex); + return null; + } + } + + private static bool IsFoder(ref PROPVARIANT kindText) + { + var kindString = GetStringFromPropVariant(ref kindText); + return string.Equals(kindString, "Folder", StringComparison.OrdinalIgnoreCase); + } + + private static string GetFilePath(ref PROPVARIANT itemUrl) + { + var filePath = GetStringFromPropVariant(ref itemUrl); + filePath = UrlToFilePathConverter.Convert(filePath); + return filePath; + } + + private static string GetStringFromPropVariant(ref PROPVARIANT propVariant) + { + if (propVariant.Anonymous.Anonymous.vt == VARENUM.VT_LPWSTR) + { + var pwszVal = propVariant.Anonymous.Anonymous.Anonymous.pwszVal; + if (pwszVal != null) + { + return pwszVal.ToString(); + } + } + + return string.Empty; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/Utils/QueryStringBuilder.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/Utils/QueryStringBuilder.cs new file mode 100644 index 0000000000..f9e9a4da42 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/Utils/QueryStringBuilder.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Globalization; +using System.Text; + +namespace Microsoft.CmdPal.Ext.Indexer.Indexer.Utils; + +internal sealed class QueryStringBuilder +{ + private const string Select = "SELECT"; + private const string Properties = "System.ItemUrl, System.ItemNameDisplay, path, System.Search.EntryID, System.Kind, System.KindText, System.Search.GatherTime, System.Search.QueryPropertyHits"; + private const string FromIndex = "FROM SystemIndex WHERE"; + private const string ScopeFileConditions = "SCOPE='file:'"; + private const string OrderConditions = "ORDER BY System.Search.Rank, System.DateModified, System.ItemNameDisplay DESC"; + private const string SelectQueryWithScope = Select + " " + Properties + " " + FromIndex + " (" + ScopeFileConditions + ")"; + private const string SelectQueryWithScopeAndOrderConditions = SelectQueryWithScope + " " + OrderConditions; + + public static string GeneratePrimingQuery() => SelectQueryWithScopeAndOrderConditions; + + public static string GenerateQuery(string searchText, uint whereId) + { + var queryStr = new StringBuilder(SelectQueryWithScope); + + // Filter by item name display only + if (!string.IsNullOrEmpty(searchText)) + { + queryStr.Append(" AND (CONTAINS(System.ItemNameDisplay, '\"") + .Append(searchText) + .Append("*\"'))"); + } + + // Always add reuse where to the query + queryStr.Append(" AND ReuseWhere(") + .Append(whereId.ToString(CultureInfo.InvariantCulture)) + .Append(") ") + .Append(OrderConditions); + + return queryStr.ToString(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/Utils/UrlToFilePathConverter.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/Utils/UrlToFilePathConverter.cs new file mode 100644 index 0000000000..bbfb8554a6 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Indexer/Utils/UrlToFilePathConverter.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.CmdPal.Ext.Indexer.Indexer.Utils; + +public class UrlToFilePathConverter +{ + public static string Convert(string url) + { + var result = url.Replace('/', '\\'); // replace all '/' to '\' + + var fileProtocolString = "file:"; + var indexProtocolFound = url.IndexOf(fileProtocolString, StringComparison.CurrentCultureIgnoreCase); + + if (indexProtocolFound != -1 && (indexProtocolFound + fileProtocolString.Length) < url.Length) + { + result = result[(indexProtocolFound + fileProtocolString.Length)..]; + } + + return result; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Microsoft.CmdPal.Ext.Indexer.csproj b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Microsoft.CmdPal.Ext.Indexer.csproj new file mode 100644 index 0000000000..c319f74ac6 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Microsoft.CmdPal.Ext.Indexer.csproj @@ -0,0 +1,37 @@ + + + + Microsoft.CmdPal.Ext.Indexer + + $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal + false + false + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Native/NativeHelpers.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Native/NativeHelpers.cs new file mode 100644 index 0000000000..9851573843 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Native/NativeHelpers.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Windows.Win32.UI.Shell.PropertiesSystem; + +namespace Microsoft.CmdPal.Ext.Indexer.Native; + +internal sealed class NativeHelpers +{ + public const uint SEEMASKINVOKEIDLIST = 12; + + internal static class PropertyKeys + { + public static readonly PROPERTYKEY PKEYItemNameDisplay = new() { fmtid = new System.Guid("B725F130-47EF-101A-A5F1-02608C9EEBAC"), pid = 10 }; + public static readonly PROPERTYKEY PKEYItemUrl = new() { fmtid = new System.Guid("49691C90-7E17-101A-A91C-08002B2ECDA9"), pid = 9 }; + public static readonly PROPERTYKEY PKEYKindText = new() { fmtid = new System.Guid("F04BEF95-C585-4197-A2B7-DF46FDC9EE6D"), pid = 100 }; + } + + internal static class OleDb + { + public static readonly Guid DbGuidDefault = new("C8B521FB-5CF3-11CE-ADE5-00AA0044773D"); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Pages/IndexerPage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Pages/IndexerPage.cs new file mode 100644 index 0000000000..87c167dfd3 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Pages/IndexerPage.cs @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using ManagedCommon; +using Microsoft.CmdPal.Ext.Indexer.Data; +using Microsoft.CmdPal.Ext.Indexer.Indexer; +using Microsoft.CmdPal.Ext.Indexer.Properties; +using Microsoft.CmdPal.Extensions; +using Microsoft.CmdPal.Extensions.Helpers; + +namespace Microsoft.CmdPal.Ext.Indexer; + +internal sealed partial class IndexerPage : DynamicListPage, IDisposable +{ + private readonly Lock _lockObject = new(); // Lock object for synchronization + private readonly List _indexerListItems = []; + + private SearchQuery _searchQuery = new(); + + private uint _queryCookie = 10; + + public IndexerPage() + { + Icon = new("\ue729"); + Name = Resources.Indexer_Title; + PlaceholderText = Resources.Indexer_PlaceholderText; + + Logger.InitializeLogger("\\CmdPal\\Indexer\\Logs"); + } + + public override void UpdateSearchText(string oldSearch, string newSearch) + { + if (oldSearch != newSearch) + { + _ = Task.Run(() => + { + Logger.LogDebug($"Update {oldSearch} -> {newSearch}"); + StartQuery(newSearch); + RaiseItemsChanged(0); + }); + } + } + + public override IListItem[] GetItems() => DoGetItems(); + + private void StartQuery(string query) + { + if (query == string.Empty) + { + return; + } + + Stopwatch stopwatch = new(); + stopwatch.Start(); + Query(query); + stopwatch.Stop(); + Logger.LogDebug($"Query time: {stopwatch.ElapsedMilliseconds} ms, query: \"{query}\""); + } + + private IListItem[] DoGetItems() + { + if (string.IsNullOrEmpty(SearchText)) + { + return []; + } + + Stopwatch stopwatch = new(); + stopwatch.Start(); + + lock (_lockObject) + { + if (_searchQuery != null) + { + var cookie = _searchQuery.Cookie; + if (cookie == _queryCookie) + { + SearchResult result; + while (!_searchQuery.SearchResults.IsEmpty && _searchQuery.SearchResults.TryDequeue(out result)) + { + _indexerListItems.Add(new IndexerListItem(new IndexerItem + { + FileName = result.ItemDisplayName, + FullPath = result.LaunchUri, + }) + { + Icon = new(result.IsFolder ? "\uE838" : "\uE8E5"), + }); + } + } + } + } + + stopwatch.Stop(); + Logger.LogDebug($"Build ListItems: {stopwatch.ElapsedMilliseconds} ms, results: {_indexerListItems.Count}, query: \"{SearchText}\""); + + return [.. _indexerListItems]; + } + + private uint Query(string searchText) + { + if (searchText == string.Empty) + { + return _queryCookie; + } + + _queryCookie++; + lock (_lockObject) + { + _searchQuery.CancelOutstandingQueries(); + _searchQuery.SearchResults.Clear(); + _indexerListItems.Clear(); + + // Just forward on to the helper with the right callback for feeding us results + // Set up the binding for the items + _searchQuery.Execute(searchText, _queryCookie); + } + + // unlock + // Wait for the query executed event + _searchQuery.WaitForQueryCompletedEvent(); + + return _queryCookie; + } + + public void Dispose() + { + _searchQuery = null; + GC.SuppressFinalize(this); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs index fd0ae55a81..ccdc69f4ad 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation +// Copyright (c) Microsoft Corporation // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -6,6 +6,7 @@ using Microsoft.CmdPal.Common.Services; using Microsoft.CmdPal.Ext.Apps.Programs; using Microsoft.CmdPal.Ext.Bookmarks; using Microsoft.CmdPal.Ext.Calc; +using Microsoft.CmdPal.Ext.Indexer; using Microsoft.CmdPal.Ext.Registry; using Microsoft.CmdPal.Ext.Settings; using Microsoft.CmdPal.Ext.Shell; @@ -87,6 +88,7 @@ public partial class App : Application services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // Models services.AddSingleton(); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj index f2b0705aef..041140c605 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj @@ -1,4 +1,4 @@ - + WinExe @@ -85,6 +85,7 @@ + diff --git a/src/modules/cmdpal/WindowsCommandPalette/Microsoft.CmdPal.UI.Poc.csproj b/src/modules/cmdpal/WindowsCommandPalette/Microsoft.CmdPal.UI.Poc.csproj index 689e36dded..298fae50c4 100644 --- a/src/modules/cmdpal/WindowsCommandPalette/Microsoft.CmdPal.UI.Poc.csproj +++ b/src/modules/cmdpal/WindowsCommandPalette/Microsoft.CmdPal.UI.Poc.csproj @@ -1,4 +1,4 @@ - + WinExe @@ -89,6 +89,7 @@ + diff --git a/src/modules/cmdpal/WindowsCommandPalette/Views/MainViewModel.xaml.cs b/src/modules/cmdpal/WindowsCommandPalette/Views/MainViewModel.xaml.cs index 2c3314824f..435e9d9128 100644 --- a/src/modules/cmdpal/WindowsCommandPalette/Views/MainViewModel.xaml.cs +++ b/src/modules/cmdpal/WindowsCommandPalette/Views/MainViewModel.xaml.cs @@ -6,6 +6,7 @@ using System.Collections.ObjectModel; using Microsoft.CmdPal.Ext.Apps.Programs; using Microsoft.CmdPal.Ext.Bookmarks; using Microsoft.CmdPal.Ext.Calc; +using Microsoft.CmdPal.Ext.Indexer; using Microsoft.CmdPal.Ext.Registry; using Microsoft.CmdPal.Ext.Settings; using Microsoft.CmdPal.Ext.Shell; @@ -50,10 +51,11 @@ public sealed class MainViewModel : IDisposable public event TypedEventHandler? GoToCommandRequested; - private readonly Dictionary _aliases = new(); + private readonly Dictionary _aliases = []; internal MainViewModel() { + BuiltInCommands.Add(new IndexerCommandsProvider()); BuiltInCommands.Add(new BookmarksCommandProvider()); BuiltInCommands.Add(new CalculatorCommandProvider()); BuiltInCommands.Add(new SettingsCommandProvider()); @@ -98,11 +100,6 @@ public sealed class MainViewModel : IDisposable handlers?.Invoke(this, null); } - private static string CreateHash(string? title, string? subtitle) - { - return title + subtitle; - } - public IEnumerable AppItems => LoadedApps ? Apps.GetItems() : []; // Okay this is definitely bad - Evaluating this re-wraps every app in the list with a new wrapper, holy fuck that's stupid @@ -120,10 +117,7 @@ public sealed class MainViewModel : IDisposable _reloadCommandProvider.Dispose(); } - private void AddAlias(CommandAlias a) - { - _aliases.Add(a.SearchPrefix, a); - } + private void AddAlias(CommandAlias a) => _aliases.Add(a.SearchPrefix, a); public bool CheckAlias(string searchText) { diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Indexer/Indexer/SearchQuery.cs b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Indexer/Indexer/SearchQuery.cs new file mode 100644 index 0000000000..872b2292cd --- /dev/null +++ b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Indexer/Indexer/SearchQuery.cs @@ -0,0 +1,496 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Concurrent; +using System.Runtime.InteropServices; +using System.Threading; +using ManagedCommon; +using Microsoft.CmdPal.Ext.Indexer.Indexer.OleDB; +using Microsoft.CmdPal.Ext.Indexer.Indexer.Utils; +using Microsoft.CmdPal.Ext.Indexer.Native; +using Windows.Win32; +using Windows.Win32.System.Com; +using Windows.Win32.System.Search; +using Windows.Win32.UI.Shell.PropertiesSystem; + +namespace Microsoft.CmdPal.Ext.Indexer.Indexer; + +internal sealed class SearchQuery : IDisposable +{ + private readonly Lock _lockObject = new(); // Lock object for synchronization + private readonly DBPROPIDSET dbPropIdSet; + + private const uint QueryTimerThreshold = 85; + private uint reuseWhereID; + private EventWaitHandle queryCompletedEvent; + private Timer queryTpTimer; + private IRowset currentRowset; + private IRowset reuseRowset; + + public uint Cookie { get; set; } + + public string SearchText { get; private set; } + + public ConcurrentQueue SearchResults { get; private set; } = []; + + public SearchQuery() + { + dbPropIdSet = new DBPROPIDSET + { + rgPropertyIDs = Marshal.AllocCoTaskMem(sizeof(uint)), // Allocate memory for the property ID array + cPropertyIDs = 1, + guidPropertySet = new Guid("AA6EE6B0-E828-11D0-B23E-00AA0047FC01"), // DBPROPSET_MSIDXS_ROWSETEXT, + }; + + // Copy the property ID into the allocated memory + Marshal.WriteInt32(dbPropIdSet.rgPropertyIDs, 8); // MSIDXSPROP_WHEREID + + Init(); + } + + private void Init() + { + // Create all the objects we will want cached + try + { + queryTpTimer = new Timer(QueryTimerCallback, this, Timeout.Infinite, Timeout.Infinite); + if (queryTpTimer == null) + { + Logger.LogError("Failed to create query timer"); + return; + } + + queryCompletedEvent = new EventWaitHandle(false, EventResetMode.ManualReset); + if (queryCompletedEvent == null) + { + Logger.LogError("Failed to create query completed event"); + return; + } + + // Execute a synchronous query on file items to prime the index and keep that handle around + PrimeIndexAndCacheWhereId(); + } + catch (Exception ex) + { + Logger.LogError("Exception at SearchUXQueryHelper Init", ex); + } + } + + public void WaitForQueryCompletedEvent() => queryCompletedEvent.WaitOne(); + + public void CancelOutstandingQueries() + { + Logger.LogDebug("Cancel query " + SearchText); + + // Are we currently doing work? If so, let's cancel + lock (_lockObject) + { + if (queryTpTimer != null) + { + queryTpTimer.Change(Timeout.Infinite, Timeout.Infinite); + queryTpTimer.Dispose(); + queryTpTimer = null; + } + + Init(); + } + } + + public void Execute(string searchText, uint cookie) + { + lock (_lockObject) + { + if (queryTpTimer != null) + { + // We cancel the outstanding query callback and queue a new one every time + queryTpTimer.Change(Timeout.Infinite, Timeout.Infinite); + SearchText = searchText; + Cookie = cookie; + + // Queue query + var fireTime = DateTime.UtcNow.AddMilliseconds(QueryTimerThreshold); + var dueTime = fireTime - DateTime.UtcNow; + queryTpTimer.Change(dueTime, Timeout.InfiniteTimeSpan); + } + } + } + + public static void QueryTimerCallback(object state) + { + var pQueryHelper = (SearchQuery)state; + pQueryHelper.ExecuteSyncInternal(); + } + + private void ExecuteSyncInternal() + { + lock (_lockObject) + { + var queryStr = QueryStringBuilder.GenerateQuery(SearchText, reuseWhereID); + try + { + // We need to generate a search query string with the search text the user entered above + if (currentRowset != null) + { + if (reuseRowset != null) + { + Marshal.ReleaseComObject(reuseRowset); + } + + // We have a previous rowset, this means the user is typing and we should store this + // recapture the where ID from this so the next ExecuteSync call will be faster + reuseRowset = currentRowset; + reuseWhereID = GetReuseWhereId(reuseRowset); + } + + currentRowset = ExecuteCommand(queryStr); + + SearchResults.Clear(); + FetchRows(); + } + catch (Exception ex) + { + Logger.LogError("Error executing query", ex); + } + finally + { + queryCompletedEvent.Set(); + } + } + } + + private bool HandleRow(IGetRow getRow, nuint rowHandle) + { + object propertyStorePtr = null; + + try + { + getRow.GetRowFromHROW(null, rowHandle, typeof(IPropertyStore).GUID, out propertyStorePtr); + + var propertyStore = (IPropertyStore)propertyStorePtr; + if (propertyStore == null) + { + Logger.LogError("Failed to get IPropertyStore interface"); + return false; + } + + var searchResult = SearchResult.Create(propertyStore); + if (searchResult == null) + { + Logger.LogError("Failed to create search result"); + return false; + } + + SearchResults.Enqueue(searchResult); + return true; + } + catch (Exception ex) + { + Logger.LogError("Error handling row", ex); + return false; + } + finally + { + // Ensure the COM object is released if not returned + if (propertyStorePtr != null) + { + Marshal.ReleaseComObject(propertyStorePtr); + } + } + } + + private ulong FetchRows() + { + if (currentRowset == null) + { + Logger.LogError("No rowset to fetch rows from"); + return 0; + } + + if (currentRowset is not IGetRow getRow) + { + Logger.LogError("Rowset does not support IGetRow interface"); + return 0; + } + + long batchSize = 5000; // Number of rows to fetch in each batch + ulong fetched = 0; + uint rowCountReturned; + var prghRows = IntPtr.Zero; + + try + { + do + { + var res = currentRowset.GetNextRows(IntPtr.Zero, 0, batchSize, out rowCountReturned, out prghRows); + if (res < 0) + { + Logger.LogError($"Error fetching rows: {res}"); + break; + } + + if (rowCountReturned == 0) + { + // No more rows to fetch + break; + } + + // Marshal the row handles + var rowHandles = new IntPtr[rowCountReturned]; + Marshal.Copy(prghRows, rowHandles, 0, (int)rowCountReturned); + + for (var i = 0; i < rowCountReturned; i++) + { + var rowHandle = Marshal.ReadIntPtr(prghRows, i * IntPtr.Size); + if (!HandleRow(getRow, (nuint)rowHandle)) + { + break; + } + } + + res = currentRowset.ReleaseRows(rowCountReturned, rowHandles, IntPtr.Zero, null, null); + if (res != 0) + { + Logger.LogError($"Error releasing rows: {res}"); + break; + } + + fetched += rowCountReturned; + + Marshal.FreeCoTaskMem(prghRows); + prghRows = IntPtr.Zero; + } + while (rowCountReturned > 0); + + return fetched; + } + catch (Exception ex) + { + Logger.LogError("Error fetching rows", ex); + return 0; + } + finally + { + if (prghRows != IntPtr.Zero) + { + Marshal.FreeCoTaskMem(prghRows); + } + } + } + + private void PrimeIndexAndCacheWhereId() + { + var queryStr = QueryStringBuilder.GeneratePrimingQuery(); + var rowset = ExecuteCommand(queryStr); + if (rowset != null) + { + if (reuseRowset != null) + { + Marshal.ReleaseComObject(reuseRowset); + } + + reuseRowset = rowset; + reuseWhereID = GetReuseWhereId(reuseRowset); + } + } + + private unsafe IRowset ExecuteCommand(string queryStr) + { + object sessionPtr = null; + object commandPtr = null; + + try + { + var session = (IDBCreateSession)DataSourceManager.GetDataSource(); + session.CreateSession(null, typeof(IDBCreateCommand).GUID, out sessionPtr); + if (sessionPtr == null) + { + Logger.LogError("CreateSession failed"); + return null; + } + + var createCommand = (IDBCreateCommand)sessionPtr; + createCommand.CreateCommand(null, typeof(ICommandText).GUID, out commandPtr); + if (commandPtr == null) + { + Logger.LogError("CreateCommand failed"); + return null; + } + + var commandText = (ICommandText)commandPtr; + if (commandText == null) + { + Logger.LogError("Failed to get ICommandText interface"); + return null; + } + + commandText.SetCommandText(in NativeHelpers.OleDb.DbGuidDefault, queryStr); + commandText.Execute(null, typeof(IRowset).GUID, null, null, out var rowsetPointer); + + return rowsetPointer as IRowset; + } + catch (Exception ex) + { + Logger.LogError("Unexpected error.", ex); + return null; + } + finally + { + // Release the command pointer + if (commandPtr != null) + { + Marshal.ReleaseComObject(commandPtr); + } + + // Release the session pointer + if (sessionPtr != null) + { + Marshal.ReleaseComObject(sessionPtr); + } + } + } + + private IRowsetInfo GetRowsetInfo(IRowset rowset) + { + if (rowset == null) + { + return null; + } + + var rowsetPtr = IntPtr.Zero; + var rowsetInfoPtr = IntPtr.Zero; + + try + { + // Get the IUnknown pointer for the IRowset object + rowsetPtr = Marshal.GetIUnknownForObject(rowset); + + // Query for IRowsetInfo interface + var rowsetInfoGuid = typeof(IRowsetInfo).GUID; + var res = Marshal.QueryInterface(rowsetPtr, in rowsetInfoGuid, out rowsetInfoPtr); + if (res != 0) + { + Logger.LogError($"Error getting IRowsetInfo interface: {res}"); + return null; + } + + // Marshal the interface pointer to the actual IRowsetInfo object + var rowsetInfo = (IRowsetInfo)Marshal.GetObjectForIUnknown(rowsetInfoPtr); + return rowsetInfo; + } + catch (Exception ex) + { + Logger.LogError($"Exception occurred while getting IRowsetInfo. ", ex); + return null; + } + finally + { + // Release the IRowsetInfo pointer if it was obtained + if (rowsetInfoPtr != IntPtr.Zero) + { + Marshal.Release(rowsetInfoPtr); // Release the IRowsetInfo pointer + } + + // Release the IUnknown pointer for the IRowset object + if (rowsetPtr != IntPtr.Zero) + { + Marshal.Release(rowsetPtr); + } + } + } + + private DBPROP? GetPropset(IRowsetInfo rowsetInfo) + { + var prgPropSetsPtr = IntPtr.Zero; + + try + { + ulong cPropertySets; + var res = rowsetInfo.GetProperties(1, [dbPropIdSet], out cPropertySets, out prgPropSetsPtr); + if (res != 0) + { + Logger.LogError($"Error getting properties: {res}"); + return null; + } + + if (cPropertySets == 0 || prgPropSetsPtr == IntPtr.Zero) + { + Logger.LogError("No property sets returned"); + return null; + } + + var firstPropSetPtr = new IntPtr(prgPropSetsPtr.ToInt64()); + var propSet = Marshal.PtrToStructure(firstPropSetPtr); + if (propSet.cProperties == 0 || propSet.rgProperties == IntPtr.Zero) + { + return null; + } + + var propPtr = new IntPtr(propSet.rgProperties.ToInt64()); + var prop = Marshal.PtrToStructure(propPtr); + return prop; + } + catch (Exception ex) + { + Logger.LogError($"Exception occurred while getting properties,", ex); + return null; + } + finally + { + // Free the property sets pointer returned by GetProperties, if necessary + if (prgPropSetsPtr != IntPtr.Zero) + { + Marshal.FreeCoTaskMem(prgPropSetsPtr); + } + } + } + + private uint GetReuseWhereId(IRowset rowset) + { + var rowsetInfo = GetRowsetInfo(rowset); + if (rowsetInfo == null) + { + return 0; + } + + var prop = GetPropset(rowsetInfo); + if (prop == null) + { + return 0; + } + + if (prop?.vValue.Anonymous.Anonymous.vt == VARENUM.VT_UI4) + { + var value = prop?.vValue.Anonymous.Anonymous.Anonymous.ulVal; + return (uint)value; + } + + return 0; + } + + public void Dispose() + { + CancelOutstandingQueries(); + + // Free the allocated memory for rgPropertyIDs + if (dbPropIdSet.rgPropertyIDs != IntPtr.Zero) + { + Marshal.FreeCoTaskMem(dbPropIdSet.rgPropertyIDs); + } + + if (reuseRowset != null) + { + Marshal.ReleaseComObject(reuseRowset); + reuseRowset = null; + } + + if (currentRowset != null) + { + Marshal.ReleaseComObject(currentRowset); + currentRowset = null; + } + + queryCompletedEvent?.Dispose(); + } +} diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Indexer/IndexerCommandsProvider.cs b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Indexer/IndexerCommandsProvider.cs new file mode 100644 index 0000000000..6e08b1b305 --- /dev/null +++ b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Indexer/IndexerCommandsProvider.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CmdPal.Ext.Indexer.Properties; +using Microsoft.CmdPal.Extensions; +using Microsoft.CmdPal.Extensions.Helpers; + +namespace Microsoft.CmdPal.Ext.Indexer; + +public partial class IndexerCommandsProvider : CommandProvider +{ + public IndexerCommandsProvider() + { + DisplayName = Resources.IndexerCommandsProvider_DisplayName; + } + + public override ICommandItem[] TopLevelCommands() + { + return [ + new CommandItem(new IndexerPage()) + { + Title = Resources.Indexer_Title, + Subtitle = Resources.Indexer_Subtitle, + } + ]; + } +} diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Indexer/NativeMethods.txt b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Indexer/NativeMethods.txt new file mode 100644 index 0000000000..f0702ba24c --- /dev/null +++ b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Indexer/NativeMethods.txt @@ -0,0 +1,11 @@ +DBID +SHOW_WINDOW_CMD +CoCreateInstance +GetErrorInfo +ICommandText +IDBCreateCommand +IDBCreateSession +IDBInitialize +IGetRow +IPropertyStore +ShellExecuteEx \ No newline at end of file diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Indexer/Properties/Resources.Designer.cs b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Indexer/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..1929650390 --- /dev/null +++ b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Indexer/Properties/Resources.Designer.cs @@ -0,0 +1,153 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.CmdPal.Ext.Indexer.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Ext.Indexer.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Copy path. + /// + internal static string Indexer_Command_CopyPath { + get { + return ResourceManager.GetString("Indexer_Command_CopyPath", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open. + /// + internal static string Indexer_Command_OpenFile { + get { + return ResourceManager.GetString("Indexer_Command_OpenFile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open path in console. + /// + internal static string Indexer_Command_OpenPathInConsole { + get { + return ResourceManager.GetString("Indexer_Command_OpenPathInConsole", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Properties. + /// + internal static string Indexer_Command_OpenProperties { + get { + return ResourceManager.GetString("Indexer_Command_OpenProperties", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open with. + /// + internal static string Indexer_Command_OpenWith { + get { + return ResourceManager.GetString("Indexer_Command_OpenWith", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show in folder. + /// + internal static string Indexer_Command_ShowInFolder { + get { + return ResourceManager.GetString("Indexer_Command_ShowInFolder", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Search for files and folders.... + /// + internal static string Indexer_PlaceholderText { + get { + return ResourceManager.GetString("Indexer_PlaceholderText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Search indexed files. + /// + internal static string Indexer_Subtitle { + get { + return ResourceManager.GetString("Indexer_Subtitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Indexer. + /// + internal static string Indexer_Title { + get { + return ResourceManager.GetString("Indexer_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Indexer commands. + /// + internal static string IndexerCommandsProvider_DisplayName { + get { + return ResourceManager.GetString("IndexerCommandsProvider_DisplayName", resourceCulture); + } + } + } +} diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Indexer/Properties/Resources.resx b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Indexer/Properties/Resources.resx new file mode 100644 index 0000000000..4c17945b16 --- /dev/null +++ b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Indexer/Properties/Resources.resx @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Indexer commands + + + Copy path + + + Open + + + Open path in console + + + Properties + + + Open with + + + Show in folder + + + Search for files and folders... + + + Search indexed files + + + Indexer + + \ No newline at end of file