File search extension (#183)

Adds support for a basic Indexer-based file search extension. 

Co-authored-by: Mike Griese <migrie@microsoft.com>
This commit is contained in:
Seraphima Zykova
2024-12-31 04:20:59 +03:00
committed by GitHub
parent dc0693e0de
commit 86ec28d9e3
31 changed files with 1775 additions and 13 deletions

View File

@@ -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

View File

@@ -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}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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<SHELLEXECUTEINFOW>(),
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();
}
}

View File

@@ -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<SHELLEXECUTEINFOW>(),
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();
}
}

View File

@@ -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();
}
}

View File

@@ -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; }
}

View File

@@ -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)),
];
}
}

View File

@@ -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;
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<RootNamespace>Microsoft.CmdPal.Ext.Indexer</RootNamespace>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\extensionsdk\Microsoft.CmdPal.Extensions.Helpers\Microsoft.CmdPal.Extensions.Helpers.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@@ -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");
}
}

View File

@@ -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<IListItem> _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);
}
}

View File

@@ -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<ICommandProvider, RegistryCommandsProvider>();
services.AddSingleton<ICommandProvider, WindowWalkerCommandsProvider>();
services.AddSingleton<ICommandProvider, WindowsSettingsCommandsProvider>();
services.AddSingleton<ICommandProvider, IndexerCommandsProvider>();
// Models
services.AddSingleton<TopLevelCommandManager>();

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<OutputType>WinExe</OutputType>
@@ -85,6 +85,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Exts\Microsoft.CmdPal.Ext.Indexer\Microsoft.CmdPal.Ext.Indexer.csproj" />
<ProjectReference Include="..\Microsoft.CmdPal.Common\Microsoft.CmdPal.Common.csproj" />
<ProjectReference Include="..\Microsoft.CmdPal.UI.ViewModels\Microsoft.CmdPal.UI.ViewModels.csproj" />

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<OutputType>WinExe</OutputType>
@@ -89,6 +89,7 @@
<ProjectReference Include="..\Exts\Microsoft.CmdPal.Ext.Bookmark\Microsoft.CmdPal.Ext.Bookmarks.csproj" />
<ProjectReference Include="..\Exts\Microsoft.CmdPal.Ext.Calc\Microsoft.CmdPal.Ext.Calc.csproj" />
<ProjectReference Include="..\Exts\Microsoft.CmdPal.Ext.CmdPalSettings\Microsoft.CmdPal.Ext.Settings.csproj" />
<ProjectReference Include="..\Exts\Microsoft.CmdPal.Ext.Indexer\Microsoft.CmdPal.Ext.Indexer.csproj" />
<ProjectReference Include="..\Exts\Microsoft.CmdPal.Ext.Registry\Microsoft.CmdPal.Ext.Registry.csproj" />
<ProjectReference Include="..\Exts\Microsoft.CmdPal.Ext.Shell\Microsoft.CmdPal.Ext.Shell.csproj" />
<ProjectReference Include="..\Exts\Microsoft.CmdPal.Ext.WindowsServices\Microsoft.CmdPal.Ext.WindowsServices.csproj" />

View File

@@ -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<object, ICommand?>? GoToCommandRequested;
private readonly Dictionary<string, CommandAlias> _aliases = new();
private readonly Dictionary<string, CommandAlias> _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<IListItem> 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)
{

View File

@@ -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<SearchResult> 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<DBPROPSET>(firstPropSetPtr);
if (propSet.cProperties == 0 || propSet.rgProperties == IntPtr.Zero)
{
return null;
}
var propPtr = new IntPtr(propSet.rgProperties.ToInt64());
var prop = Marshal.PtrToStructure<DBPROP>(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();
}
}

View File

@@ -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,
}
];
}
}

View File

@@ -0,0 +1,11 @@
DBID
SHOW_WINDOW_CMD
CoCreateInstance
GetErrorInfo
ICommandText
IDBCreateCommand
IDBCreateSession
IDBInitialize
IGetRow
IPropertyStore
ShellExecuteEx

View File

@@ -0,0 +1,153 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Microsoft.CmdPal.Ext.Indexer.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// 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() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[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;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Copy path.
/// </summary>
internal static string Indexer_Command_CopyPath {
get {
return ResourceManager.GetString("Indexer_Command_CopyPath", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open.
/// </summary>
internal static string Indexer_Command_OpenFile {
get {
return ResourceManager.GetString("Indexer_Command_OpenFile", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open path in console.
/// </summary>
internal static string Indexer_Command_OpenPathInConsole {
get {
return ResourceManager.GetString("Indexer_Command_OpenPathInConsole", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Properties.
/// </summary>
internal static string Indexer_Command_OpenProperties {
get {
return ResourceManager.GetString("Indexer_Command_OpenProperties", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open with.
/// </summary>
internal static string Indexer_Command_OpenWith {
get {
return ResourceManager.GetString("Indexer_Command_OpenWith", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Show in folder.
/// </summary>
internal static string Indexer_Command_ShowInFolder {
get {
return ResourceManager.GetString("Indexer_Command_ShowInFolder", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Search for files and folders....
/// </summary>
internal static string Indexer_PlaceholderText {
get {
return ResourceManager.GetString("Indexer_PlaceholderText", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Search indexed files.
/// </summary>
internal static string Indexer_Subtitle {
get {
return ResourceManager.GetString("Indexer_Subtitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Indexer.
/// </summary>
internal static string Indexer_Title {
get {
return ResourceManager.GetString("Indexer_Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Indexer commands.
/// </summary>
internal static string IndexerCommandsProvider_DisplayName {
get {
return ResourceManager.GetString("IndexerCommandsProvider_DisplayName", resourceCulture);
}
}
}
}

View File

@@ -0,0 +1,150 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="IndexerCommandsProvider_DisplayName" xml:space="preserve">
<value>Indexer commands</value>
</data>
<data name="Indexer_Command_CopyPath" xml:space="preserve">
<value>Copy path</value>
</data>
<data name="Indexer_Command_OpenFile" xml:space="preserve">
<value>Open</value>
</data>
<data name="Indexer_Command_OpenPathInConsole" xml:space="preserve">
<value>Open path in console</value>
</data>
<data name="Indexer_Command_OpenProperties" xml:space="preserve">
<value>Properties</value>
</data>
<data name="Indexer_Command_OpenWith" xml:space="preserve">
<value>Open with</value>
</data>
<data name="Indexer_Command_ShowInFolder" xml:space="preserve">
<value>Show in folder</value>
</data>
<data name="Indexer_PlaceholderText" xml:space="preserve">
<value>Search for files and folders...</value>
</data>
<data name="Indexer_Subtitle" xml:space="preserve">
<value>Search indexed files</value>
</data>
<data name="Indexer_Title" xml:space="preserve">
<value>Indexer</value>
</data>
</root>