diff --git a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs
index 17c2dc511..ce067e8a0 100644
--- a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs
+++ b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs
@@ -127,5 +127,81 @@ namespace Flow.Launcher.Plugin.SharedCommands
#endif
}
}
+
+ ///
+ /// This checks whether a given string is a directory path or network location string.
+ /// It does not check if location actually exists.
+ ///
+ public static bool IsLocationPathString(string querySearchString)
+ {
+ if (string.IsNullOrEmpty(querySearchString))
+ return false;
+
+ // // shared folder location, and not \\\location\
+ if (querySearchString.Length >= 3
+ && querySearchString.StartsWith(@"\\")
+ && char.IsLetter(querySearchString[2]))
+ return true;
+
+ // c:\
+ if (querySearchString.Length == 3
+ && char.IsLetter(querySearchString[0])
+ && querySearchString[1] == ':'
+ && querySearchString[2] == '\\')
+ return true;
+
+ // c:\\
+ if (querySearchString.Length >= 4
+ && char.IsLetter(querySearchString[0])
+ && querySearchString[1] == ':'
+ && querySearchString[2] == '\\'
+ && char.IsLetter(querySearchString[3]))
+ return true;
+
+ return false;
+ }
+
+ ///
+ /// Gets the previous level directory from a path string.
+ /// Checks that previous level directory exists and returns it
+ /// as a path string, or empty string if doesn't exit
+ ///
+ public static string GetPreviousExistingDirectory(Func locationExists, string path)
+ {
+ var previousDirectoryPath = "";
+ var index = path.LastIndexOf('\\');
+ if (index > 0 && index < (path.Length - 1))
+ {
+ previousDirectoryPath = path.Substring(0, index + 1);
+ if (!locationExists(previousDirectoryPath))
+ {
+ return "";
+ }
+ }
+ else
+ {
+ return "";
+ }
+
+ return previousDirectoryPath;
+ }
+
+ ///
+ /// Returns the previous level directory if path incomplete (does not end with '\').
+ /// Does not check if previous level directory exists.
+ /// Returns passed in string if is complete path
+ ///
+ public static string ReturnPreviousDirectoryIfIncompleteString(string path)
+ {
+ if (!path.EndsWith("\\"))
+ {
+ // not full path, get previous level directory string
+ var indexOfSeparator = path.LastIndexOf('\\');
+
+ return path.Substring(0, indexOfSeparator + 1);
+ }
+
+ return path;
+ }
}
}
diff --git a/Flow.Launcher.Test/Flow.Launcher.Test.csproj b/Flow.Launcher.Test/Flow.Launcher.Test.csproj
index eb26fccf2..e970c47b9 100644
--- a/Flow.Launcher.Test/Flow.Launcher.Test.csproj
+++ b/Flow.Launcher.Test/Flow.Launcher.Test.csproj
@@ -39,6 +39,7 @@
+
diff --git a/Flow.Launcher.Test/Plugins/ExplorerTest.cs b/Flow.Launcher.Test/Plugins/ExplorerTest.cs
new file mode 100644
index 000000000..0a1dece4f
--- /dev/null
+++ b/Flow.Launcher.Test/Plugins/ExplorerTest.cs
@@ -0,0 +1,279 @@
+using Flow.Launcher.Plugin;
+using Flow.Launcher.Plugin.Explorer;
+using Flow.Launcher.Plugin.Explorer.Search;
+using Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo;
+using Flow.Launcher.Plugin.Explorer.Search.WindowsIndex;
+using Flow.Launcher.Plugin.SharedCommands;
+using NUnit.Framework;
+using System;
+using System.Collections.Generic;
+
+namespace Flow.Launcher.Test.Plugins
+{
+ ///
+ /// These tests require the use of CSearchManager class from Microsoft.Search.Interop.
+ /// Windows Search service needs to be running to complete the tests
+ ///
+ [TestFixture]
+ public class ExplorerTest
+ {
+ private List MethodWindowsIndexSearchReturnsZeroResults(Query dummyQuery, string dummyString)
+ {
+ return new List();
+ }
+
+ private List MethodDirectoryInfoClassSearchReturnsTwoResults(Query dummyQuery, string dummyString)
+ {
+ return new List
+ {
+ new Result
+ {
+ Title="Result 1"
+ },
+
+ new Result
+ {
+ Title="Result 2"
+ }
+ };
+ }
+
+ private bool PreviousLocationExistsReturnsTrue(string dummyString) => true;
+
+ private bool PreviousLocationNotExistReturnsFalse(string dummyString) => false;
+
+ [TestCase("C:\\SomeFolder\\", "directory='file:C:\\SomeFolder\\'")]
+ public void GivenWindowsIndexSearch_WhenProvidedFolderPath_ThenQueryWhereRestrictionsShouldUseDirectoryString(string path, string expectedString)
+ {
+ // Given
+ var queryConstructor = new QueryConstructor(new Settings());
+
+ // When
+ var folderPath = path;
+ var result = queryConstructor.QueryWhereRestrictionsForTopLevelDirectorySearch(folderPath);
+
+ // Then
+ Assert.IsTrue(result == expectedString,
+ $"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " +
+ $"Actual: {result}{Environment.NewLine}");
+ }
+
+ [TestCase("C:\\", "SELECT TOP 100 System.FileName, System.ItemPathDisplay, System.ItemType FROM SystemIndex WHERE directory='file:C:\\'")]
+ [TestCase("C:\\SomeFolder\\", "SELECT TOP 100 System.FileName, System.ItemPathDisplay, System.ItemType FROM SystemIndex WHERE directory='file:C:\\SomeFolder\\'")]
+ public void GivenWindowsIndexSearch_WhenSearchTypeIsTopLevelDirectorySearch_ThenQueryShouldUseExpectedString(string folderPath, string expectedString)
+ {
+ // Given
+ var queryConstructor = new QueryConstructor(new Settings());
+
+ //When
+ var queryString = queryConstructor.QueryForTopLevelDirectorySearch(folderPath);
+
+ // Then
+ Assert.IsTrue(queryString == expectedString,
+ $"Expected string: {expectedString}{Environment.NewLine} " +
+ $"Actual string was: {queryString}{Environment.NewLine}");
+ }
+
+ [TestCase("C:\\SomeFolder\\flow.launcher.sln", "SELECT TOP 100 System.FileName, System.ItemPathDisplay, System.ItemType " +
+ "FROM SystemIndex WHERE (System.FileName LIKE 'flow.launcher.sln%' " +
+ "OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033))" +
+ " AND directory='file:C:\\SomeFolder'")]
+ public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificItem_ThenQueryShouldUseExpectedString(
+ string userSearchString, string expectedString)
+ {
+ // Given
+ var queryConstructor = new QueryConstructor(new Settings());
+
+ //When
+ var queryString = queryConstructor.QueryForTopLevelDirectorySearch(userSearchString);
+
+ // Then
+ Assert.IsTrue(queryString == expectedString,
+ $"Expected string: {expectedString}{Environment.NewLine} " +
+ $"Actual string was: {queryString}{Environment.NewLine}");
+ }
+
+ [TestCase("C:\\SomeFolder\\SomeApp", "(System.FileName LIKE 'SomeApp%' " +
+ "OR CONTAINS(System.FileName,'\"SomeApp*\"',1033))" +
+ " AND directory='file:C:\\SomeFolder'")]
+ public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificItem_ThenQueryWhereRestrictionsShouldUseDirectoryString(
+ string userSearchString, string expectedString)
+ {
+ // Given
+ var queryConstructor = new QueryConstructor(new Settings());
+
+ //When
+ var queryString = queryConstructor.QueryWhereRestrictionsForTopLevelDirectorySearch(userSearchString);
+
+ // Then
+ Assert.IsTrue(queryString == expectedString,
+ $"Expected string: {expectedString}{Environment.NewLine} " +
+ $"Actual string was: {queryString}{Environment.NewLine}");
+ }
+
+ [TestCase("scope='file:'")]
+ public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryWhereRestrictionsShouldUseScopeString(string expectedString)
+ {
+ // Given
+ var queryConstructor = new QueryConstructor(new Settings());
+
+ //When
+ var resultString = queryConstructor.QueryWhereRestrictionsForAllFilesAndFoldersSearch();
+
+ // Then
+ Assert.IsTrue(resultString == expectedString,
+ $"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " +
+ $"Actual string was: {resultString}{Environment.NewLine}");
+ }
+
+ [TestCase("flow.launcher.sln", "SELECT TOP 100 \"System.FileName\", \"System.ItemPathDisplay\", \"System.ItemType\" " +
+ "FROM \"SystemIndex\" WHERE (System.FileName LIKE 'flow.launcher.sln%' " +
+ "OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:'")]
+ public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryShouldUseExpectedString(
+ string userSearchString, string expectedString)
+ {
+ // Given
+ var queryConstructor = new QueryConstructor(new Settings());
+
+ //When
+ var resultString = queryConstructor.QueryForAllFilesAndFolders(userSearchString);
+
+ // Then
+ Assert.IsTrue(resultString == expectedString,
+ $"Expected query string: {expectedString}{Environment.NewLine} " +
+ $"Actual string was: {resultString}{Environment.NewLine}");
+ }
+
+ [TestCase]
+ public void GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMethodShouldContinueDirectoryInfoClassSearch()
+ {
+ // Given
+ var searchManager = new SearchManager(new Settings(), new PluginInitContext());
+
+ // When
+ var results = searchManager.TopLevelDirectorySearchBehaviour(
+ MethodWindowsIndexSearchReturnsZeroResults,
+ MethodDirectoryInfoClassSearchReturnsTwoResults,
+ false,
+ new Query(),
+ "string not used");
+
+ // Then
+ Assert.IsTrue(results.Count == 2,
+ $"Expected to have 2 results from DirectoryInfoClassSearch {Environment.NewLine} " +
+ $"Actual number of results is {results.Count} {Environment.NewLine}");
+ }
+
+ [TestCase]
+ public void GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMethodShouldNotContinueDirectoryInfoClassSearch()
+ {
+ // Given
+ var searchManager = new SearchManager(new Settings(), new PluginInitContext());
+
+ // When
+ var results = searchManager.TopLevelDirectorySearchBehaviour(
+ MethodWindowsIndexSearchReturnsZeroResults,
+ MethodDirectoryInfoClassSearchReturnsTwoResults,
+ true,
+ new Query(),
+ "string not used");
+
+ // Then
+ Assert.IsTrue(results.Count == 0,
+ $"Expected to have 0 results because location is indexed {Environment.NewLine} " +
+ $"Actual number of results is {results.Count} {Environment.NewLine}");
+ }
+
+ [TestCase(@"c:\\", false)]
+ [TestCase(@"i:\", true)]
+ [TestCase(@"\c:\", false)]
+ [TestCase(@"cc:\", false)]
+ [TestCase(@"\\\SomeNetworkLocation\", false)]
+ [TestCase("RandomFile", false)]
+ public void WhenGivenQuerySearchString_ThenShouldIndicateIfIsLocationPathString(string querySearchString, bool expectedResult)
+ {
+ // When, Given
+ var result = FilesFolders.IsLocationPathString(querySearchString);
+
+ //Then
+ Assert.IsTrue(result == expectedResult,
+ $"Expected query search string check result is: {expectedResult} {Environment.NewLine} " +
+ $"Actual check result is {result} {Environment.NewLine}");
+
+ }
+
+ [TestCase(@"C:\SomeFolder\SomeApp", true, @"C:\SomeFolder\")]
+ [TestCase(@"C:\SomeFolder\SomeApp\SomeFile", true, @"C:\SomeFolder\SomeApp\")]
+ [TestCase(@"C:\NonExistentFolder\SomeApp", false, "")]
+ public void GivenAPartialPath_WhenPreviousLevelDirectoryExists_ThenShouldReturnThePreviousDirectoryPathString(
+ string path, bool previousDirectoryExists, string expectedString)
+ {
+ // When
+ Func previousLocationExists = null;
+ if (previousDirectoryExists)
+ {
+ previousLocationExists = PreviousLocationExistsReturnsTrue;
+ }
+ else
+ {
+ previousLocationExists = PreviousLocationNotExistReturnsFalse;
+ }
+
+ // Given
+ var previousDirectoryPath = FilesFolders.GetPreviousExistingDirectory(previousLocationExists, path);
+
+ //Then
+ Assert.IsTrue(previousDirectoryPath == expectedString,
+ $"Expected path string: {expectedString} {Environment.NewLine} " +
+ $"Actual path string is {previousDirectoryPath} {Environment.NewLine}");
+ }
+
+ [TestCase(@"C:\NonExistentFolder\SomeApp", @"C:\NonExistentFolder\")]
+ [TestCase(@"C:\NonExistentFolder\SomeApp\", @"C:\NonExistentFolder\SomeApp\")]
+ public void WhenGivenAPath_ThenShouldReturnThePreviousDirectoryPathIfIncompleteOrOriginalString(
+ string path, string expectedString)
+ {
+ var returnedPath = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(path);
+
+ //Then
+ Assert.IsTrue(returnedPath == expectedString,
+ $"Expected path string: {expectedString} {Environment.NewLine} " +
+ $"Actual path string is {returnedPath} {Environment.NewLine}");
+ }
+
+ [TestCase("c:\\SomeFolder\\>", "scope='file:c:\\SomeFolder'")]
+ [TestCase("c:\\SomeFolder\\>SomeName", "(System.FileName LIKE 'SomeName%' " +
+ "OR CONTAINS(System.FileName,'\"SomeName*\"',1033)) AND " +
+ "scope='file:c:\\SomeFolder'")]
+ public void GivenWindowsIndexSearch_WhenSearchPatternHotKeyIsSearchAll_ThenQueryWhereRestrictionsShouldUseScopeString(string path, string expectedString)
+ {
+ // Given
+ var queryConstructor = new QueryConstructor(new Settings());
+
+ //When
+ var resultString = queryConstructor.QueryWhereRestrictionsForTopLevelDirectoryAllFilesAndFoldersSearch(path);
+
+ // Then
+ Assert.IsTrue(resultString == expectedString,
+ $"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " +
+ $"Actual string was: {resultString}{Environment.NewLine}");
+ }
+
+ [TestCase("c:\\somefolder\\>somefile","*somefile*")]
+ [TestCase("c:\\somefolder\\somefile", "somefile*")]
+ [TestCase("c:\\somefolder\\", "*")]
+ public void GivenDirectoryInfoSearch_WhenSearchPatternHotKeyIsSearchAll_ThenSearchCriteriaShouldUseCriteriaString(string path, string expectedString)
+ {
+ // Given
+ var criteriaConstructor = new DirectoryInfoSearch(new PluginInitContext());
+
+ //When
+ var resultString = criteriaConstructor.ConstructSearchCriteria(path);
+
+ // Then
+ Assert.IsTrue(resultString == expectedString,
+ $"Expected criteria string: {expectedString}{Environment.NewLine} " +
+ $"Actual criteria string was: {resultString}{Environment.NewLine}");
+ }
+ }
+}
diff --git a/Flow.Launcher.sln b/Flow.Launcher.sln
index f668b2764..b244c97de 100644
--- a/Flow.Launcher.sln
+++ b/Flow.Launcher.sln
@@ -1,4 +1,3 @@
-
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29806.167
@@ -18,9 +17,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher", "Flow.Launc
{1EE20B48-82FB-48A2-8086-675D6DDAB4F0} = {1EE20B48-82FB-48A2-8086-675D6DDAB4F0}
{0B9DE348-9361-4940-ADB6-F5953BFFCCEC} = {0B9DE348-9361-4940-ADB6-F5953BFFCCEC}
{FDB3555B-58EF-4AE6-B5F1-904719637AB4} = {FDB3555B-58EF-4AE6-B5F1-904719637AB4}
+ {F9C4C081-4CC3-4146-95F1-E102B4E10A5F} = {F9C4C081-4CC3-4146-95F1-E102B4E10A5F}
{59BD9891-3837-438A-958D-ADC7F91F6F7E} = {59BD9891-3837-438A-958D-ADC7F91F6F7E}
{C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0} = {C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0}
- {787B8AA6-CA93-4C84-96FE-DF31110AD1C4} = {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}
{F35190AA-4758-4D9E-A193-E3BDF6AD3567} = {F35190AA-4758-4D9E-A193-E3BDF6AD3567}
{9B130CC5-14FB-41FF-B310-0A95B6894C37} = {9B130CC5-14FB-41FF-B310-0A95B6894C37}
{FDED22C8-B637-42E8-824A-63B5B6E05A3A} = {FDED22C8-B637-42E8-824A-63B5B6E05A3A}
@@ -41,8 +40,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.WebSea
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.ControlPanel", "Plugins\Flow.Launcher.Plugin.ControlPanel\Flow.Launcher.Plugin.ControlPanel.csproj", "{1EE20B48-82FB-48A2-8086-675D6DDAB4F0}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.Folder", "Plugins\Flow.Launcher.Plugin.Folder\Flow.Launcher.Plugin.Folder.csproj", "{787B8AA6-CA93-4C84-96FE-DF31110AD1C4}"
-EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.PluginIndicator", "Plugins\Flow.Launcher.Plugin.PluginIndicator\Flow.Launcher.Plugin.PluginIndicator.csproj", "{FDED22C8-B637-42E8-824A-63B5B6E05A3A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.Sys", "Plugins\Flow.Launcher.Plugin.Sys\Flow.Launcher.Plugin.Sys.csproj", "{0B9DE348-9361-4940-ADB6-F5953BFFCCEC}"
@@ -58,11 +55,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.gitattributes = .gitattributes
.gitignore = .gitignore
appveyor.yml = appveyor.yml
+ Scripts\flowlauncher.nuspec = Scripts\flowlauncher.nuspec
LICENSE = LICENSE
Scripts\post_build.ps1 = Scripts\post_build.ps1
README.md = README.md
SolutionAssemblyInfo.cs = SolutionAssemblyInfo.cs
- Scripts\flowlauncher.nuspec = Scripts\flowlauncher.nuspec
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HelloWorldCSharp", "Plugins\HelloWorldCSharp\HelloWorldCSharp.csproj", "{03FFA443-5F50-48D5-8869-F3DF316803AA}"
@@ -75,6 +72,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.Calcul
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "HelloWorldFSharp", "Plugins\HelloWorldFSharp\HelloWorldFSharp.fsproj", "{30DDA7D9-3712-44F4-BD18-DC1C05B2DD9E}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.Explorer", "Plugins\Flow.Launcher.Plugin.Explorer\Flow.Launcher.Plugin.Explorer.csproj", "{F9C4C081-4CC3-4146-95F1-E102B4E10A5F}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -193,18 +192,6 @@ Global
{1EE20B48-82FB-48A2-8086-675D6DDAB4F0}.Release|x64.Build.0 = Release|Any CPU
{1EE20B48-82FB-48A2-8086-675D6DDAB4F0}.Release|x86.ActiveCfg = Release|Any CPU
{1EE20B48-82FB-48A2-8086-675D6DDAB4F0}.Release|x86.Build.0 = Release|Any CPU
- {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Debug|x64.ActiveCfg = Debug|Any CPU
- {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Debug|x64.Build.0 = Debug|Any CPU
- {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Debug|x86.ActiveCfg = Debug|Any CPU
- {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Debug|x86.Build.0 = Debug|Any CPU
- {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Release|Any CPU.Build.0 = Release|Any CPU
- {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Release|x64.ActiveCfg = Release|Any CPU
- {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Release|x64.Build.0 = Release|Any CPU
- {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Release|x86.ActiveCfg = Release|Any CPU
- {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Release|x86.Build.0 = Release|Any CPU
{FDED22C8-B637-42E8-824A-63B5B6E05A3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FDED22C8-B637-42E8-824A-63B5B6E05A3A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FDED22C8-B637-42E8-824A-63B5B6E05A3A}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -326,6 +313,18 @@ Global
{30DDA7D9-3712-44F4-BD18-DC1C05B2DD9E}.Release|x64.Build.0 = Release|Any CPU
{30DDA7D9-3712-44F4-BD18-DC1C05B2DD9E}.Release|x86.ActiveCfg = Release|Any CPU
{30DDA7D9-3712-44F4-BD18-DC1C05B2DD9E}.Release|x86.Build.0 = Release|Any CPU
+ {F9C4C081-4CC3-4146-95F1-E102B4E10A5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F9C4C081-4CC3-4146-95F1-E102B4E10A5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F9C4C081-4CC3-4146-95F1-E102B4E10A5F}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F9C4C081-4CC3-4146-95F1-E102B4E10A5F}.Debug|x64.Build.0 = Debug|Any CPU
+ {F9C4C081-4CC3-4146-95F1-E102B4E10A5F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F9C4C081-4CC3-4146-95F1-E102B4E10A5F}.Debug|x86.Build.0 = Debug|Any CPU
+ {F9C4C081-4CC3-4146-95F1-E102B4E10A5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F9C4C081-4CC3-4146-95F1-E102B4E10A5F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F9C4C081-4CC3-4146-95F1-E102B4E10A5F}.Release|x64.ActiveCfg = Release|Any CPU
+ {F9C4C081-4CC3-4146-95F1-E102B4E10A5F}.Release|x64.Build.0 = Release|Any CPU
+ {F9C4C081-4CC3-4146-95F1-E102B4E10A5F}.Release|x86.ActiveCfg = Release|Any CPU
+ {F9C4C081-4CC3-4146-95F1-E102B4E10A5F}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -335,7 +334,6 @@ Global
{FDB3555B-58EF-4AE6-B5F1-904719637AB4} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87}
{403B57F2-1856-4FC7-8A24-36AB346B763E} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87}
{1EE20B48-82FB-48A2-8086-675D6DDAB4F0} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87}
- {787B8AA6-CA93-4C84-96FE-DF31110AD1C4} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87}
{FDED22C8-B637-42E8-824A-63B5B6E05A3A} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87}
{0B9DE348-9361-4940-ADB6-F5953BFFCCEC} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87}
{A3DCCBCA-ACC1-421D-B16E-210896234C26} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87}
@@ -346,6 +344,7 @@ Global
{9B130CC5-14FB-41FF-B310-0A95B6894C37} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87}
{59BD9891-3837-438A-958D-ADC7F91F6F7E} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87}
{30DDA7D9-3712-44F4-BD18-DC1C05B2DD9E} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87}
+ {F9C4C081-4CC3-4146-95F1-E102B4E10A5F} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F26ACB50-3F6C-4907-B0C9-1ADACC1D0DED}
diff --git a/Flow.Launcher/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj
index 6fe1b817d..987a685ac 100644
--- a/Flow.Launcher/Flow.Launcher.csproj
+++ b/Flow.Launcher/Flow.Launcher.csproj
@@ -83,9 +83,11 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs
new file mode 100644
index 000000000..90a3fb2e8
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs
@@ -0,0 +1,287 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Threading.Tasks;
+using System.Windows;
+using Flow.Launcher.Infrastructure.Logger;
+using Flow.Launcher.Plugin.SharedCommands;
+using Flow.Launcher.Plugin.Explorer.Search;
+using Flow.Launcher.Plugin.Explorer.Search.FolderLinks;
+using System.Linq;
+using System.Reflection;
+
+namespace Flow.Launcher.Plugin.Explorer
+{
+ internal class ContextMenu : IContextMenu
+ {
+ private PluginInitContext Context { get; set; }
+
+ private Settings Settings { get; set; }
+
+ public ContextMenu(PluginInitContext context, Settings settings)
+ {
+ Context = context;
+ Settings = settings;
+ }
+
+ public List LoadContextMenus(Result selectedResult)
+ {
+ var contextMenus = new List();
+ if (selectedResult.ContextData is SearchResult record)
+ {
+ if (record.Type == ResultType.File)
+ contextMenus.Add(CreateOpenWithEditorResult(record));
+
+ if (record.Type == ResultType.Folder && record.WindowsIndexed)
+ contextMenus.Add(CreateAddToIndexSearchExclusionListResult(record));
+
+ contextMenus.Add(CreateOpenContainingFolderResult(record));
+
+ contextMenus.Add(CreateOpenWindowsIndexingOptions());
+
+ if (record.ShowIndexState)
+ contextMenus.Add(new Result {Title = "From index search: " + (record.WindowsIndexed ? "Yes" : "No"),
+ SubTitle = "Location: " + record.FullPath,
+ Score = 501, IcoPath = Constants.IndexImagePath});
+
+ var icoPath = (record.Type == ResultType.File) ? Constants.FileImagePath : Constants.FolderImagePath;
+ var fileOrFolder = (record.Type == ResultType.File) ? "file" : "folder";
+ contextMenus.Add(new Result
+ {
+ Title = Context.API.GetTranslation("plugin_explorer_copypath"),
+ SubTitle = $"Copy the current {fileOrFolder} path to clipboard",
+ Action = (context) =>
+ {
+ try
+ {
+ Clipboard.SetText(record.FullPath);
+ return true;
+ }
+ catch (Exception e)
+ {
+ var message = "Fail to set text in clipboard";
+ LogException(message, e);
+ Context.API.ShowMsg(message);
+ return false;
+ }
+ },
+ IcoPath = Constants.CopyImagePath
+ });
+
+ contextMenus.Add(new Result
+ {
+ Title = Context.API.GetTranslation("plugin_explorer_copyfilefolder") + $" {fileOrFolder}",
+ SubTitle = $"Copy the {fileOrFolder} to clipboard",
+ Action = (context) =>
+ {
+ try
+ {
+ Clipboard.SetFileDropList(new System.Collections.Specialized.StringCollection { record.FullPath });
+ return true;
+ }
+ catch (Exception e)
+ {
+ var message = $"Fail to set {fileOrFolder} in clipboard";
+ LogException(message, e);
+ Context.API.ShowMsg(message);
+ return false;
+ }
+
+ },
+ IcoPath = icoPath
+ });
+
+ if (record.Type == ResultType.File || record.Type == ResultType.Folder)
+ contextMenus.Add(new Result
+ {
+ Title = Context.API.GetTranslation("plugin_explorer_deletefilefolder") + $" {fileOrFolder}",
+ SubTitle = Context.API.GetTranslation("plugin_explorer_deletefilefolder_subtitle") + $" {fileOrFolder}",
+ Action = (context) =>
+ {
+ try
+ {
+ if (record.Type == ResultType.File)
+ File.Delete(record.FullPath);
+ else
+ Directory.Delete(record.FullPath, true);
+ }
+ catch (Exception e)
+ {
+ var message = $"Fail to delete {fileOrFolder} at {record.FullPath}";
+ LogException(message, e);
+ Context.API.ShowMsg(message);
+ return false;
+ }
+
+ return true;
+ },
+ IcoPath = Constants.DeleteFileFolderImagePath
+ });
+
+ if (record.Type == ResultType.File && CanRunAsDifferentUser(record.FullPath))
+ contextMenus.Add(new Result
+ {
+ Title = Context.API.GetTranslation("plugin_explorer_runasdifferentuser"),
+ SubTitle = Context.API.GetTranslation("plugin_explorer_runasdifferentuser_subtitle"),
+ Action = (context) =>
+ {
+ try
+ {
+ Task.Run(() => ShellCommand.RunAsDifferentUser(record.FullPath.SetProcessStartInfo()));
+ }
+ catch (FileNotFoundException e)
+ {
+ var name = "Plugin: Folder";
+ var message = $"File not found: {e.Message}";
+ Context.API.ShowMsg(name, message);
+ }
+
+ return true;
+ },
+ IcoPath = Constants.DifferentUserIconImagePath
+ });
+ }
+
+ return contextMenus;
+ }
+
+ private Result CreateOpenContainingFolderResult(SearchResult record)
+ {
+ return new Result
+ {
+ Title = Context.API.GetTranslation("plugin_explorer_opencontainingfolder"),
+ SubTitle = Context.API.GetTranslation("plugin_explorer_opencontainingfolder_subtitle"),
+ Action = _ =>
+ {
+ try
+ {
+ Process.Start("explorer.exe", $" /select,\"{record.FullPath}\"");
+ }
+ catch (Exception e)
+ {
+ var message = $"Fail to open file at {record.FullPath}";
+ LogException(message, e);
+ Context.API.ShowMsg(message);
+ return false;
+ }
+
+ return true;
+ },
+ IcoPath = Constants.FolderImagePath
+ };
+ }
+
+ private Result CreateOpenWithEditorResult(SearchResult record)
+ {
+ string editorPath = "Notepad.exe"; // TODO add the ability to create a custom editor
+
+ var name = Context.API.GetTranslation("plugin_explorer_openwitheditor")
+ + " " + Path.GetFileNameWithoutExtension(editorPath);
+
+ return new Result
+ {
+ Title = name,
+ Action = _ =>
+ {
+ try
+ {
+ Process.Start(editorPath, record.FullPath);
+ return true;
+ }
+ catch (Exception e)
+ {
+ var message = $"Failed to open editor for file at {record.FullPath}";
+ LogException(message, e);
+ Context.API.ShowMsg(message);
+ return false;
+ }
+ },
+ IcoPath = Constants.FileImagePath
+ };
+ }
+
+ private Result CreateAddToIndexSearchExclusionListResult(SearchResult record)
+ {
+ return new Result
+ {
+ Title = Context.API.GetTranslation("plugin_explorer_excludefromindexsearch"),
+ SubTitle = Context.API.GetTranslation("plugin_explorer_path") + " " + record.FullPath,
+ Action = _ =>
+ {
+ if(!Settings.IndexSearchExcludedSubdirectoryPaths.Any(x => x.Path == record.FullPath))
+ Settings.IndexSearchExcludedSubdirectoryPaths.Add(new FolderLink { Path = record.FullPath });
+
+ var pluginDirectory = Directory.GetParent(Assembly.GetExecutingAssembly().Location.ToString());
+
+ var iconPath = pluginDirectory + "\\" + Constants.ExplorerIconImagePath;
+
+ Task.Run(() =>
+ {
+ Context.API.ShowMsg(Context.API.GetTranslation("plugin_explorer_excludedfromindexsearch_msg"),
+ Context.API.GetTranslation("plugin_explorer_path") +
+ " " + record.FullPath, iconPath);
+
+ // so the new path can be persisted to storage and not wait till next ViewModel save.
+ Context.API.SaveAppAllSettings();
+ });
+
+ return false;
+ },
+ IcoPath = Constants.ExcludeFromIndexImagePath
+ };
+ }
+
+ private Result CreateOpenWindowsIndexingOptions()
+ {
+ return new Result
+ {
+ Title = Context.API.GetTranslation("plugin_explorer_openindexingoptions"),
+ SubTitle = Context.API.GetTranslation("plugin_explorer_openindexingoptions_subtitle"),
+ Action = _ =>
+ {
+ try
+ {
+ var psi = new ProcessStartInfo
+ {
+ FileName = "control.exe",
+ UseShellExecute = true,
+ Arguments = "srchadmin.dll"
+ };
+
+ Process.Start(psi);
+ return true;
+ }
+ catch (Exception e)
+ {
+ var message = Context.API.GetTranslation("plugin_explorer_openindexingoptions_errormsg");
+ LogException(message, e);
+ Context.API.ShowMsg(message);
+ return false;
+ }
+ },
+ IcoPath = Constants.IndexingOptionsIconImagePath
+ };
+ }
+
+ public void LogException(string message, Exception e)
+ {
+ Log.Exception($"|Flow.Launcher.Plugin.Folder.ContextMenu|{message}", e);
+ }
+
+ private bool CanRunAsDifferentUser(string path)
+ {
+ switch (Path.GetExtension(path))
+ {
+ case ".exe":
+ case ".bat":
+ case ".msi":
+ return true;
+
+ default:
+ return false;
+
+ }
+ }
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Folder/Flow.Launcher.Plugin.Folder.csproj b/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj
similarity index 64%
rename from Plugins/Flow.Launcher.Plugin.Folder/Flow.Launcher.Plugin.Folder.csproj
rename to Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj
index a05b5d49d..bd2a047eb 100644
--- a/Plugins/Flow.Launcher.Plugin.Folder/Flow.Launcher.Plugin.Folder.csproj
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj
@@ -1,37 +1,23 @@
+ Library
netcoreapp3.1
- {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}
- Properties
- Flow.Launcher.Plugin.Folder
- Flow.Launcher.Plugin.Folder
+ true
true
false
- false
+
+
-
-
- true
- full
- false
- ..\..\Output\Debug\Plugins\Flow.Launcher.Plugin.Folder\
- DEBUG;TRACE
- prompt
- 4
- false
+
+
+ ..\..\Output\Debug\Plugins\Flow.Launcher.Plugin.Explorer
-
-
- pdbonly
- true
- ..\..\Output\Release\Plugins\Flow.Launcher.Plugin.Folder\
- TRACE
- prompt
- 4
- false
+
+
+ ..\..\Output\Release\Plugins\Flow.Launcher.Plugin.Explorer
-
+
PreserveNewest
@@ -39,69 +25,87 @@
-
- MSBuild:Compile
- Designer
-
+
+ PreserveNewest
+
+
+
+ PreserveNewest
+
+
+
+ PreserveNewest
+
+
PreserveNewest
+
Always
+
PreserveNewest
-
- Always
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+
+ PreserveNewest
+
+
MSBuild:Compile
Designer
PreserveNewest
+
MSBuild:Compile
Designer
PreserveNewest
+
MSBuild:Compile
Designer
PreserveNewest
+
MSBuild:Compile
Designer
PreserveNewest
+
MSBuild:Compile
Designer
PreserveNewest
+
MSBuild:Compile
Designer
PreserveNewest
+
+
+
+
+
-
-
-
- PreserveNewest
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Folder/Images/copy.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/copy.png
similarity index 100%
rename from Plugins/Flow.Launcher.Plugin.Folder/Images/copy.png
rename to Plugins/Flow.Launcher.Plugin.Explorer/Images/copy.png
diff --git a/Plugins/Flow.Launcher.Plugin.Folder/Images/deletefilefolder.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/deletefilefolder.png
similarity index 100%
rename from Plugins/Flow.Launcher.Plugin.Folder/Images/deletefilefolder.png
rename to Plugins/Flow.Launcher.Plugin.Explorer/Images/deletefilefolder.png
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Images/excludeindexpath.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/excludeindexpath.png
new file mode 100644
index 000000000..8578ac7fb
Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.Explorer/Images/excludeindexpath.png differ
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Images/explorer.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/explorer.png
new file mode 100644
index 000000000..552b2c5bd
Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.Explorer/Images/explorer.png differ
diff --git a/Plugins/Flow.Launcher.Plugin.Folder/Images/file.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/file.png
similarity index 100%
rename from Plugins/Flow.Launcher.Plugin.Folder/Images/file.png
rename to Plugins/Flow.Launcher.Plugin.Explorer/Images/file.png
diff --git a/Plugins/Flow.Launcher.Plugin.Folder/Images/folder.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/folder.png
similarity index 100%
rename from Plugins/Flow.Launcher.Plugin.Folder/Images/folder.png
rename to Plugins/Flow.Launcher.Plugin.Explorer/Images/folder.png
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Images/index.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/index.png
new file mode 100644
index 000000000..c63fc8069
Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.Explorer/Images/index.png differ
diff --git a/Plugins/Flow.Launcher.Plugin.Folder/Images/user.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/user.png
similarity index 100%
rename from Plugins/Flow.Launcher.Plugin.Folder/Images/user.png
rename to Plugins/Flow.Launcher.Plugin.Explorer/Images/user.png
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Images/windowsindexingoptions.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/windowsindexingoptions.png
new file mode 100644
index 000000000..78a015715
Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.Explorer/Images/windowsindexingoptions.png differ
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/de.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/de.xaml
new file mode 100644
index 000000000..2209b5bed
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/de.xaml
@@ -0,0 +1,14 @@
+
+
+
+ Löschen
+ Bearbeiten
+ Hinzufügen
+
+
+ Bitte wähle eine Ordnerverknüpfung
+ Bist du sicher {0} zu löschen?
+
+
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml
new file mode 100644
index 000000000..bc3f5664a
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml
@@ -0,0 +1,39 @@
+
+
+
+ Please select a folder link
+ Are you sure you want to delete {0}?
+
+
+ Delete
+ Edit
+ Add
+ Quick Folder Access Paths
+ Index Search Excluded Paths
+ Indexing Options
+
+
+
+ Explorer
+ Search and manage files and folders. Explorer utilises Windows Index Search
+
+
+ Copy path
+ Copy
+ Delete
+ Path:
+ Delete the selected
+ Run as different user
+ Run the selected using a different user account
+ Open containing folder
+ Opens the location that contains the file or folder
+ Open With Editor:
+ Exclude current and sub-directories from Index Search
+ Excluded from Index Search
+ Open Windows Indexing Options
+ Manage indexed files and folders
+ Failed to open Windows Indexing Options
+
+
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pl.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pl.xaml
new file mode 100644
index 000000000..9a6b38f6f
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pl.xaml
@@ -0,0 +1,14 @@
+
+
+
+ Usuń
+ Edytuj
+ Dodaj
+
+
+ Musisz wybrać któryś folder z listy
+ Czy jesteś pewien że chcesz usunąć {0}?
+
+
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/tr.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/tr.xaml
new file mode 100644
index 000000000..d6744fdd4
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/tr.xaml
@@ -0,0 +1,14 @@
+
+
+
+ Sil
+ Düzenle
+ Ekle
+
+
+ Lütfen bir klasör bağlantısı seçin
+ {0} bağlantısını silmek istediğinize emin misiniz?
+
+
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-cn.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-cn.xaml
new file mode 100644
index 000000000..7f516861e
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-cn.xaml
@@ -0,0 +1,14 @@
+
+
+
+ 删除
+ 编辑
+ 添加
+
+
+ 请选择一个文件夹
+ 你确定要删除{0}吗?
+
+
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-tw.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-tw.xaml
new file mode 100644
index 000000000..1455aa676
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-tw.xaml
@@ -0,0 +1,14 @@
+
+
+
+ 刪除
+ 編輯
+ 新增
+
+
+ 請選擇一個資料夾
+ 你確認要刪除{0}嗎?
+
+
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs
new file mode 100644
index 000000000..30a06e882
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs
@@ -0,0 +1,58 @@
+using Flow.Launcher.Infrastructure.Storage;
+using Flow.Launcher.Plugin.Explorer.Search;
+using Flow.Launcher.Plugin.Explorer.ViewModels;
+using Flow.Launcher.Plugin.Explorer.Views;
+using System.Collections.Generic;
+using System.Windows.Controls;
+
+namespace Flow.Launcher.Plugin.Explorer
+{
+ public class Main : ISettingProvider, IPlugin, ISavable, IContextMenu, IPluginI18n
+ {
+ internal PluginInitContext Context { get; set; }
+
+ internal Settings Settings;
+
+ private SettingsViewModel viewModel;
+
+ private IContextMenu contextMenu;
+
+ public Control CreateSettingPanel()
+ {
+ return new ExplorerSettings(viewModel);
+ }
+
+ public void Init(PluginInitContext context)
+ {
+ Context = context;
+ viewModel = new SettingsViewModel(context);
+ Settings = viewModel.Settings;
+ contextMenu = new ContextMenu(Context, Settings);
+ }
+
+ public List LoadContextMenus(Result selectedResult)
+ {
+ return contextMenu.LoadContextMenus(selectedResult);
+ }
+
+ public List Query(Query query)
+ {
+ return new SearchManager(Settings, Context).Search(query);
+ }
+
+ public void Save()
+ {
+ viewModel.Save();
+ }
+
+ public string GetTranslatedPluginTitle()
+ {
+ return Context.API.GetTranslation("plugin_explorer_plugin_name");
+ }
+
+ public string GetTranslatedPluginDescription()
+ {
+ return Context.API.GetTranslation("plugin_explorer_plugin_description");
+ }
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs
new file mode 100644
index 000000000..9c8a300bc
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Flow.Launcher.Plugin.Explorer.Search
+{
+ internal static class Constants
+ {
+ internal const string FolderImagePath = "Images\\folder.png";
+ internal const string FileImagePath = "Images\\file.png";
+ internal const string DeleteFileFolderImagePath = "Images\\deletefilefolder.png";
+ internal const string CopyImagePath = "Images\\copy.png";
+ internal const string IndexImagePath = "Images\\index.png";
+ internal const string ExcludeFromIndexImagePath = "Images\\excludeindexpath.png";
+ internal const string ExplorerIconImagePath = "Images\\explorer.png";
+ internal const string DifferentUserIconImagePath = "Images\\user.png";
+ internal const string IndexingOptionsIconImagePath = "Images\\windowsindexingoptions.png";
+
+ internal const string DefaultFolderSubtitleString = "Ctrl + Enter to open the directory";
+
+ internal const char AllFilesFolderSearchWildcard = '>';
+
+ internal const char DirectorySeperator = '\\';
+
+ internal const string WindowsIndexingOptions = "srchadmin.dll";
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs
new file mode 100644
index 000000000..b198395b8
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs
@@ -0,0 +1,96 @@
+using Flow.Launcher.Infrastructure.Logger;
+using Flow.Launcher.Plugin.SharedCommands;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo
+{
+ public class DirectoryInfoSearch
+ {
+ private readonly ResultManager resultManager;
+
+ public DirectoryInfoSearch(PluginInitContext context)
+ {
+ resultManager = new ResultManager(context);
+ }
+
+ internal List TopLevelDirectorySearch(Query query, string search)
+ {
+ var criteria = ConstructSearchCriteria(search);
+
+ if (search.LastIndexOf(Constants.AllFilesFolderSearchWildcard) > search.LastIndexOf(Constants.DirectorySeperator))
+ return DirectorySearch(SearchOption.AllDirectories, query, search, criteria);
+
+ return DirectorySearch(SearchOption.TopDirectoryOnly, query, search, criteria);
+ }
+
+ public string ConstructSearchCriteria(string search)
+ {
+ string incompleteName = "";
+
+ if (!search.EndsWith(Constants.DirectorySeperator))
+ {
+ var indexOfSeparator = search.LastIndexOf(Constants.DirectorySeperator);
+
+ incompleteName = search.Substring(indexOfSeparator + 1).ToLower();
+
+ if (incompleteName.StartsWith(Constants.AllFilesFolderSearchWildcard))
+ incompleteName = "*" + incompleteName.Substring(1);
+ }
+
+ incompleteName += "*";
+
+ return incompleteName;
+ }
+
+ private List DirectorySearch(SearchOption searchOption, Query query, string search, string searchCriteria)
+ {
+ var results = new List();
+
+ var path = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(search);
+
+ var folderList = new List();
+ var fileList = new List();
+
+ try
+ {
+ var directoryInfo = new System.IO.DirectoryInfo(path);
+ var fileSystemInfos = directoryInfo.GetFileSystemInfos(searchCriteria, searchOption);
+
+ foreach (var fileSystemInfo in fileSystemInfos)
+ {
+ if ((fileSystemInfo.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) continue;
+
+ if (fileSystemInfo is System.IO.DirectoryInfo)
+ {
+ folderList.Add(resultManager.CreateFolderResult(fileSystemInfo.Name, Constants.DefaultFolderSubtitleString, fileSystemInfo.FullName, query, true, false));
+ }
+ else
+ {
+ fileList.Add(resultManager.CreateFileResult(fileSystemInfo.FullName, query, true, false));
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ if (e is UnauthorizedAccessException || e is ArgumentException)
+ {
+ results.Add(new Result { Title = e.Message, Score = 501 });
+
+ return results;
+ }
+
+#if DEBUG // Please investigate and handle error from DirectoryInfo search
+ throw e;
+#else
+ Log.Exception($"|Flow.Launcher.Plugin.Explorer.DirectoryInfoSearch|Error from performing DirectoryInfoSearch", e);
+#endif
+ }
+
+ // Intial ordering, this order can be updated later by UpdateResultView.MainViewModel based on history of user selection.
+ return results.Concat(folderList.OrderBy(x => x.Title)).Concat(fileList.OrderBy(x => x.Title)).ToList();
+ }
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/EnvironmentVariables.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/EnvironmentVariables.cs
new file mode 100644
index 000000000..5ba76bd7d
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/EnvironmentVariables.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace Flow.Launcher.Plugin.Explorer.Search
+{
+ public static class EnvironmentVariables
+ {
+ internal static bool IsEnvironmentVariableSearch(string search)
+ {
+ return LoadEnvironmentStringPaths().Count > 0 && search.StartsWith("%") && !search.Substring(1).Contains("%");
+ }
+
+ internal static Dictionary LoadEnvironmentStringPaths()
+ {
+ var envStringPaths = new Dictionary();
+
+ foreach (DictionaryEntry special in Environment.GetEnvironmentVariables())
+ {
+ if (Directory.Exists(special.Value.ToString()))
+ {
+ envStringPaths.Add(special.Key.ToString().ToLower(), special.Value.ToString());
+ }
+ }
+
+ return envStringPaths;
+ }
+
+ internal static string TranslateEnvironmentVariablePath(string environmentVariablePath)
+ {
+ var envStringPaths = LoadEnvironmentStringPaths();
+ var splitSearch = environmentVariablePath.Substring(1).Split("%");
+ var exactEnvStringPath = splitSearch[0];
+
+ // if there are more than 2 % characters in the query, don't bother
+ if (splitSearch.Length == 2 && envStringPaths.ContainsKey(exactEnvStringPath))
+ {
+ var queryPartToReplace = $"%{exactEnvStringPath}%";
+ var expandedPath = envStringPaths[exactEnvStringPath];
+ // replace the %envstring% part of the query with its expanded equivalent
+ return environmentVariablePath.Replace(queryPartToReplace, expandedPath);
+ }
+
+ return environmentVariablePath;
+ }
+
+ internal static List GetEnvironmentStringPathSuggestions(string querySearch, Query query, PluginInitContext context)
+ {
+ var results = new List();
+
+ var environmentVariables = LoadEnvironmentStringPaths();
+ var search = querySearch;
+
+ if (querySearch.EndsWith("%") && search.Length > 1)
+ {
+ // query starts and ends with a %, find an exact match from env-string paths
+ search = querySearch.Substring(1, search.Length - 2);
+
+ if (environmentVariables.ContainsKey(search))
+ {
+ var expandedPath = environmentVariables[search];
+
+ results.Add(new ResultManager(context).CreateFolderResult($"%{search}%", expandedPath, expandedPath, query));
+
+ return results;
+ }
+ }
+
+ if (querySearch == "%")
+ {
+ search = ""; // Get all paths
+ }
+ else
+ {
+ search = search.Substring(1);
+ }
+
+ foreach (var p in environmentVariables)
+ {
+ if (p.Key.StartsWith(search))
+ {
+ results.Add(new ResultManager(context).CreateFolderResult($"%{p.Key}%", p.Value, p.Value, query));
+ }
+ }
+ return results;
+ }
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/FolderLink.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/FolderLink.cs
new file mode 100644
index 000000000..379b5848f
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/FolderLink.cs
@@ -0,0 +1,29 @@
+using Newtonsoft.Json;
+using System;
+using System.Linq;
+
+namespace Flow.Launcher.Plugin.Explorer.Search.FolderLinks
+{
+ [JsonObject(MemberSerialization.OptIn)]
+ public class FolderLink
+ {
+ [JsonProperty]
+ public string Path { get; set; }
+
+ public string Nickname
+ {
+ get
+ {
+ var path = Path.EndsWith(Constants.DirectorySeperator) ? Path[0..^1] : Path;
+
+ if (path.EndsWith(':'))
+ return path[0..^1] + " Drive";
+
+ return path.Split(new[] { System.IO.Path.DirectorySeparatorChar }, StringSplitOptions.None)
+ .Last()
+ + " (" + System.IO.Path.GetDirectoryName(Path) + ")";
+ }
+ }
+ }
+
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/QuickFolderAccess.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/QuickFolderAccess.cs
new file mode 100644
index 000000000..689c865c0
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/QuickFolderAccess.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Flow.Launcher.Plugin.Explorer.Search.FolderLinks
+{
+ public class QuickFolderAccess
+ {
+ internal List FolderList(Query query, List folderLinks, PluginInitContext context)
+ {
+ if (string.IsNullOrEmpty(query.Search))
+ return folderLinks
+ .Select(item =>
+ new ResultManager(context)
+ .CreateFolderResult(item.Nickname, Constants.DefaultFolderSubtitleString, item.Path, query))
+ .ToList();
+
+ string search = query.Search.ToLower();
+
+ var queriedFolderLinks = folderLinks.Where(x => x.Nickname.StartsWith(search, StringComparison.OrdinalIgnoreCase));
+
+ return queriedFolderLinks.Select(item =>
+ new ResultManager(context)
+ .CreateFolderResult(item.Nickname, Constants.DefaultFolderSubtitleString, item.Path, query))
+ .ToList();
+ }
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs
new file mode 100644
index 000000000..e2fd1c7da
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs
@@ -0,0 +1,135 @@
+using Flow.Launcher.Infrastructure;
+using Flow.Launcher.Plugin.SharedCommands;
+using System;
+using System.IO;
+using System.Linq;
+using System.Windows;
+
+namespace Flow.Launcher.Plugin.Explorer.Search
+{
+ public class ResultManager
+ {
+ private readonly PluginInitContext context;
+
+ public ResultManager(PluginInitContext context)
+ {
+ this.context = context;
+ }
+ internal Result CreateFolderResult(string title, string subtitle, string path, Query query, bool showIndexState = false, bool windowsIndexed = false)
+ {
+ return new Result
+ {
+ Title = title,
+ IcoPath = path,
+ SubTitle = subtitle,
+ TitleHighlightData = StringMatcher.FuzzySearch(query.Search, title).MatchData,
+ Action = c =>
+ {
+ if (c.SpecialKeyState.CtrlPressed)
+ {
+ try
+ {
+ FilesFolders.OpenPath(path);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show(ex.Message, "Could not start " + path);
+ return false;
+ }
+ }
+
+ string changeTo = path.EndsWith(Constants.DirectorySeperator) ? path : path + Constants.DirectorySeperator;
+ context.API.ChangeQuery(string.IsNullOrEmpty(query.ActionKeyword) ?
+ changeTo :
+ query.ActionKeyword + " " + changeTo);
+ return false;
+ },
+ ContextData = new SearchResult { Type = ResultType.Folder, FullPath = path, ShowIndexState = showIndexState, WindowsIndexed = windowsIndexed }
+ };
+ }
+
+ internal Result CreateOpenCurrentFolderResult(string path, bool windowsIndexed = false)
+ {
+ var retrievedDirectoryPath = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(path);
+
+ var folderName = retrievedDirectoryPath.TrimEnd(Constants.DirectorySeperator).Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.None).Last();
+
+ if (retrievedDirectoryPath.EndsWith(":\\"))
+ {
+ var driveLetter = path.Substring(0, 1).ToUpper();
+ folderName = driveLetter + " drive";
+ }
+
+ var title = "Open current directory";
+
+ if (retrievedDirectoryPath != path)
+ title = "Open " + folderName;
+
+
+ var subtitleFolderName = folderName;
+
+ // ie. max characters can be displayed without subtitle cutting off: "Program Files (x86)"
+ if (folderName.Length > 19)
+ subtitleFolderName = "the directory";
+
+ return new Result
+ {
+ Title = title,
+ SubTitle = $"Use > to search within {subtitleFolderName}, " +
+ $"* to search for file extensions or >* to combine both searches.",
+ IcoPath = retrievedDirectoryPath,
+ Score = 500,
+ Action = c =>
+ {
+ FilesFolders.OpenPath(retrievedDirectoryPath);
+ return true;
+ },
+ ContextData = new SearchResult { Type = ResultType.Folder, FullPath = retrievedDirectoryPath, ShowIndexState = true, WindowsIndexed = windowsIndexed }
+ };
+ }
+
+ internal Result CreateFileResult(string filePath, Query query, bool showIndexState = false, bool windowsIndexed = false)
+ {
+ var result = new Result
+ {
+ Title = Path.GetFileName(filePath),
+ SubTitle = filePath,
+ IcoPath = filePath,
+ TitleHighlightData = StringMatcher.FuzzySearch(query.Search, Path.GetFileName(filePath)).MatchData,
+ Action = c =>
+ {
+ try
+ {
+ FilesFolders.OpenPath(filePath);
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show(ex.Message, "Could not start " + filePath);
+ }
+
+ return true;
+ },
+ ContextData = new SearchResult { Type = ResultType.File, FullPath = filePath, ShowIndexState = showIndexState, WindowsIndexed = windowsIndexed }
+ };
+ return result;
+ }
+ }
+
+ internal class SearchResult
+ {
+ public string FullPath { get; set; }
+ public ResultType Type { get; set; }
+
+ public bool WindowsIndexed { get; set; }
+
+ public bool ShowIndexState { get; set; }
+ }
+
+ internal enum ResultType
+ {
+ Volume,
+ Folder,
+ File
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs
new file mode 100644
index 000000000..4d3f11967
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs
@@ -0,0 +1,126 @@
+using Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo;
+using Flow.Launcher.Plugin.Explorer.Search.FolderLinks;
+using Flow.Launcher.Plugin.Explorer.Search.WindowsIndex;
+using Flow.Launcher.Plugin.SharedCommands;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Flow.Launcher.Plugin.Explorer.Search
+{
+ public class SearchManager
+ {
+ private readonly PluginInitContext context;
+
+ private readonly IndexSearch indexSearch;
+
+ private readonly QuickFolderAccess quickFolderAccess = new QuickFolderAccess();
+
+ private readonly ResultManager resultManager;
+
+ private readonly Settings settings;
+
+ public SearchManager(Settings settings, PluginInitContext context)
+ {
+ this.context = context;
+ indexSearch = new IndexSearch(context);
+ resultManager = new ResultManager(context);
+ this.settings = settings;
+ }
+
+ internal List Search(Query query)
+ {
+ var results = new List();
+
+ var querySearch = query.Search;
+
+ var quickFolderLinks = quickFolderAccess.FolderList(query, settings.QuickFolderAccessLinks, context);
+
+ if (quickFolderLinks.Count > 0)
+ return quickFolderLinks;
+
+ if (string.IsNullOrEmpty(query.Search))
+ return results;
+
+ if (!FilesFolders.IsLocationPathString(querySearch))
+ return WindowsIndexFilesAndFoldersSearch(query, querySearch);
+
+ var locationPath = query.Search;
+
+ if (EnvironmentVariables.IsEnvironmentVariableSearch(locationPath))
+ return EnvironmentVariables.GetEnvironmentStringPathSuggestions(locationPath, query, context);
+
+ // Query is a location path with a full environment variable, eg. %appdata%\somefolder\
+ if (locationPath.Substring(1).Contains("%"))
+ locationPath = EnvironmentVariables.TranslateEnvironmentVariablePath(locationPath);
+
+ if (!FilesFolders.LocationExists(FilesFolders.ReturnPreviousDirectoryIfIncompleteString(locationPath)))
+ return results;
+
+ var useIndexSearch = UseWindowsIndexForDirectorySearch(locationPath);
+
+ results.Add(resultManager.CreateOpenCurrentFolderResult(locationPath, useIndexSearch));
+
+ results.AddRange(TopLevelDirectorySearchBehaviour(WindowsIndexTopLevelFolderSearch,
+ DirectoryInfoClassSearch,
+ useIndexSearch,
+ query,
+ locationPath));
+
+ return results;
+ }
+
+ private List DirectoryInfoClassSearch(Query query, string querySearch)
+ {
+ var directoryInfoSearch = new DirectoryInfoSearch(context);
+
+ return directoryInfoSearch.TopLevelDirectorySearch(query, querySearch);
+ }
+
+ public List TopLevelDirectorySearchBehaviour(
+ Func> windowsIndexSearch,
+ Func> directoryInfoClassSearch,
+ bool useIndexSearch,
+ Query query,
+ string querySearchString)
+ {
+ if (!useIndexSearch)
+ return directoryInfoClassSearch(query, querySearchString);
+
+ return windowsIndexSearch(query, querySearchString);
+ }
+
+ private List WindowsIndexFilesAndFoldersSearch(Query query, string querySearchString)
+ {
+ var queryConstructor = new QueryConstructor(settings);
+
+ return indexSearch.WindowsIndexSearch(querySearchString,
+ queryConstructor.CreateQueryHelper().ConnectionString,
+ queryConstructor.QueryForAllFilesAndFolders,
+ query);
+ }
+
+ private List WindowsIndexTopLevelFolderSearch(Query query, string path)
+ {
+ var queryConstructor = new QueryConstructor(settings);
+
+ return indexSearch.WindowsIndexSearch(path,
+ queryConstructor.CreateQueryHelper().ConnectionString,
+ queryConstructor.QueryForTopLevelDirectorySearch,
+ query);
+ }
+
+ private bool UseWindowsIndexForDirectorySearch(string locationPath)
+ {
+ if (!settings.UseWindowsIndexForDirectorySearch)
+ return false;
+
+ if (settings.IndexSearchExcludedSubdirectoryPaths
+ .Any(x => FilesFolders.ReturnPreviousDirectoryIfIncompleteString(locationPath)
+ .StartsWith(x.Path, StringComparison.OrdinalIgnoreCase)))
+ return false;
+
+ return indexSearch.PathIsIndexed(locationPath);
+ }
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs
new file mode 100644
index 000000000..5401c2845
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs
@@ -0,0 +1,117 @@
+using Flow.Launcher.Infrastructure.Logger;
+using Microsoft.Search.Interop;
+using System;
+using System.Collections.Generic;
+using System.Data.OleDb;
+using System.Linq;
+using System.Text.RegularExpressions;
+
+namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
+{
+ internal class IndexSearch
+ {
+ private readonly object _lock = new object();
+
+ private OleDbConnection conn;
+
+ private OleDbCommand command;
+
+ private OleDbDataReader dataReaderResults;
+
+ private readonly ResultManager resultManager;
+
+ // Reserved keywords in oleDB
+ private readonly string reservedStringPattern = @"^[\/\\\$\%]+$";
+
+ internal IndexSearch(PluginInitContext context)
+ {
+ resultManager = new ResultManager(context);
+ }
+
+ internal List ExecuteWindowsIndexSearch(string indexQueryString, string connectionString, Query query)
+ {
+ var folderResults = new List();
+ var fileResults = new List();
+ var results = new List();
+
+ try
+ {
+ using (conn = new OleDbConnection(connectionString))
+ {
+ conn.Open();
+
+ using (command = new OleDbCommand(indexQueryString, conn))
+ {
+ // Results return as an OleDbDataReader.
+ using (dataReaderResults = command.ExecuteReader())
+ {
+ if (dataReaderResults.HasRows)
+ {
+ while (dataReaderResults.Read())
+ {
+ if (dataReaderResults.GetValue(0) != DBNull.Value && dataReaderResults.GetValue(1) != DBNull.Value)
+ {
+ if (dataReaderResults.GetString(2) == "Directory")
+ {
+ folderResults.Add(resultManager.CreateFolderResult(
+ dataReaderResults.GetString(0),
+ Constants.DefaultFolderSubtitleString,
+ dataReaderResults.GetString(1),
+ query, true, true));
+ }
+ else
+ {
+ fileResults.Add(resultManager.CreateFileResult(dataReaderResults.GetString(1), query, true, true));
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ catch (InvalidOperationException e)
+ {
+ // Internal error from ExecuteReader(): Connection closed.
+ LogException("Internal error from ExecuteReader()", e);
+ }
+ catch (Exception e)
+ {
+ LogException("General error from performing index search", e);
+ }
+
+ // Intial ordering, this order can be updated later by UpdateResultView.MainViewModel based on history of user selection.
+ return results.Concat(folderResults.OrderBy(x => x.Title)).Concat(fileResults.OrderBy(x => x.Title)).ToList(); ;
+ }
+
+ internal List WindowsIndexSearch(string searchString, string connectionString, Func constructQuery, Query query)
+ {
+ var regexMatch = Regex.Match(searchString, reservedStringPattern);
+
+ if (regexMatch.Success)
+ return new List();
+
+ lock (_lock)
+ {
+ var constructedQuery = constructQuery(searchString);
+ return ExecuteWindowsIndexSearch(constructedQuery, connectionString, query);
+ }
+ }
+
+ internal bool PathIsIndexed(string path)
+ {
+ var csm = new CSearchManager();
+ var indexManager = csm.GetCatalog("SystemIndex").GetCrawlScopeManager();
+ return indexManager.IncludedInCrawlScope(path) > 0;
+ }
+
+ private void LogException(string message, Exception e)
+ {
+#if DEBUG // Please investigate and handle error from index search
+ throw e;
+#else
+ Log.Exception($"|Flow.Launcher.Plugin.Explorer.IndexSearch|{message}", e);
+#endif
+ }
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs
new file mode 100644
index 000000000..f4480eea7
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs
@@ -0,0 +1,121 @@
+using Microsoft.Search.Interop;
+
+namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
+{
+ public class QueryConstructor
+ {
+ private readonly Settings settings;
+
+ private const string SystemIndex = "SystemIndex";
+
+ public QueryConstructor(Settings settings)
+ {
+ this.settings = settings;
+ }
+
+ public CSearchQueryHelper CreateBaseQuery()
+ {
+ var baseQuery = CreateQueryHelper();
+
+ // Set the number of results we want. Don't set this property if all results are needed.
+ baseQuery.QueryMaxResults = settings.MaxResult;
+
+ // Set list of columns we want to display, getting the path presently
+ baseQuery.QuerySelectColumns = "System.FileName, System.ItemPathDisplay, System.ItemType";
+
+ // Filter based on file name
+ baseQuery.QueryContentProperties = "System.FileName";
+
+ // Set sorting order
+ //baseQuery.QuerySorting = "System.ItemType DESC";
+
+ return baseQuery;
+ }
+
+ internal CSearchQueryHelper CreateQueryHelper()
+ {
+ // This uses the Microsoft.Search.Interop assembly
+ var manager = new CSearchManager();
+
+ // SystemIndex catalog is the default catalog in Windows
+ var catalogManager = manager.GetCatalog(SystemIndex);
+
+ // Get the ISearchQueryHelper which will help us to translate AQS --> SQL necessary to query the indexer
+ var queryHelper = catalogManager.GetQueryHelper();
+
+ return queryHelper;
+ }
+
+ ///
+ /// Set the required WHERE clause restriction to search on the first level of a specified directory.
+ ///
+ public string QueryWhereRestrictionsForTopLevelDirectorySearch(string path)
+ {
+ var searchDepth = $"directory='file:";
+
+ return QueryWhereRestrictionsFromLocationPath(path, searchDepth);
+ }
+
+ ///
+ /// Set the required WHERE clause restriction to search all files and subfolders of a specified directory.
+ ///
+ public string QueryWhereRestrictionsForTopLevelDirectoryAllFilesAndFoldersSearch(string path)
+ {
+ var searchDepth = $"scope='file:";
+
+ return QueryWhereRestrictionsFromLocationPath(path, searchDepth);
+ }
+
+ private string QueryWhereRestrictionsFromLocationPath(string path, string searchDepth)
+ {
+ if (path.EndsWith(Constants.DirectorySeperator))
+ return searchDepth + $"{path}'";
+
+ var indexOfSeparator = path.LastIndexOf(Constants.DirectorySeperator);
+
+ var itemName = path.Substring(indexOfSeparator + 1);
+
+ if (itemName.StartsWith(Constants.AllFilesFolderSearchWildcard))
+ itemName = itemName.Substring(1);
+
+ var previousLevelDirectory = path.Substring(0, indexOfSeparator);
+
+ if (string.IsNullOrEmpty(itemName))
+ return searchDepth + $"{previousLevelDirectory}'";
+
+ return $"(System.FileName LIKE '{itemName}%' " +
+ $"OR CONTAINS(System.FileName,'\"{itemName}*\"',1033)) AND " +
+ searchDepth + $"{previousLevelDirectory}'";
+ }
+
+ ///
+ /// Search will be performed on all folders and files on the first level of a specified directory.
+ ///
+ public string QueryForTopLevelDirectorySearch(string path)
+ {
+ string query = "SELECT TOP " + settings.MaxResult + $" {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE ";
+
+ if (path.LastIndexOf(Constants.AllFilesFolderSearchWildcard) > path.LastIndexOf(Constants.DirectorySeperator))
+ return query + QueryWhereRestrictionsForTopLevelDirectoryAllFilesAndFoldersSearch(path);
+
+ return query + QueryWhereRestrictionsForTopLevelDirectorySearch(path);
+ }
+
+ ///
+ /// Search will be performed on all folders and files based on user's search keywords.
+ ///
+ public string QueryForAllFilesAndFolders(string userSearchString)
+ {
+ // Generate SQL from constructed parameters, converting the userSearchString from AQS->WHERE clause
+ return CreateBaseQuery().GenerateSQLFromUserQuery(userSearchString) + " AND " + QueryWhereRestrictionsForAllFilesAndFoldersSearch();
+ }
+
+ ///
+ /// Set the required WHERE clause restriction to search for all files and folders.
+ ///
+ public string QueryWhereRestrictionsForAllFilesAndFoldersSearch()
+ {
+ return $"scope='file:'";
+ }
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs
new file mode 100644
index 000000000..d4bfdbbfc
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs
@@ -0,0 +1,21 @@
+using Flow.Launcher.Plugin.Explorer.Search.FolderLinks;
+using Newtonsoft.Json;
+using System.Collections.Generic;
+
+namespace Flow.Launcher.Plugin.Explorer
+{
+ public class Settings
+ {
+ [JsonProperty]
+ public int MaxResult { get; set; } = 100;
+
+ [JsonProperty]
+ public List QuickFolderAccessLinks { get; set; } = new List();
+
+ [JsonProperty]
+ public bool UseWindowsIndexForDirectorySearch { get; set; } = true;
+
+ [JsonProperty]
+ public List IndexSearchExcludedSubdirectoryPaths { get; set; } = new List();
+ }
+}
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs
new file mode 100644
index 000000000..a5e81716a
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs
@@ -0,0 +1,44 @@
+using Flow.Launcher.Infrastructure.Storage;
+using Flow.Launcher.Plugin.Explorer.Search;
+using Flow.Launcher.Plugin.Explorer.Search.FolderLinks;
+using System.Diagnostics;
+
+namespace Flow.Launcher.Plugin.Explorer.ViewModels
+{
+ public class SettingsViewModel
+ {
+ private readonly PluginJsonStorage storage;
+
+ internal Settings Settings { get; set; }
+
+ internal PluginInitContext Context { get; set; }
+
+ public SettingsViewModel(PluginInitContext context)
+ {
+ Context = context;
+ storage = new PluginJsonStorage();
+ Settings = storage.Load();
+ }
+
+ public void Save()
+ {
+ storage.Save();
+ }
+
+ internal void RemoveFolderLinkFromQuickFolders(FolderLink selectedRow) => Settings.QuickFolderAccessLinks.Remove(selectedRow);
+
+ internal void RemoveFolderLinkFromExcludedIndexPaths(FolderLink selectedRow) => Settings.IndexSearchExcludedSubdirectoryPaths.Remove(selectedRow);
+
+ internal void OpenWindowsIndexingOptions()
+ {
+ var psi = new ProcessStartInfo
+ {
+ FileName = "control.exe",
+ UseShellExecute = true,
+ Arguments = Constants.WindowsIndexingOptions
+ };
+
+ Process.Start(psi);
+ }
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml
new file mode 100644
index 000000000..5ba1a063b
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml.cs
new file mode 100644
index 000000000..11c8f6514
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml.cs
@@ -0,0 +1,245 @@
+using Flow.Launcher.Plugin.Explorer.Search.FolderLinks;
+using Flow.Launcher.Plugin.Explorer.ViewModels;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.IO;
+using System.Linq;
+using System.Windows;
+using System.Windows.Forms;
+using DataFormats = System.Windows.DataFormats;
+using DragDropEffects = System.Windows.DragDropEffects;
+using DragEventArgs = System.Windows.DragEventArgs;
+using MessageBox = System.Windows.MessageBox;
+
+namespace Flow.Launcher.Plugin.Explorer.Views
+{
+ ///
+ /// Interaction logic for ExplorerSettings.xaml
+ ///
+ public partial class ExplorerSettings
+ {
+ private readonly SettingsViewModel viewModel;
+ public ExplorerSettings(SettingsViewModel viewModel)
+ {
+ InitializeComponent();
+
+ this.viewModel = viewModel;
+
+ lbxFolderLinks.ItemsSource = this.viewModel.Settings.QuickFolderAccessLinks;
+
+ lbxExcludedPaths.ItemsSource = this.viewModel.Settings.IndexSearchExcludedSubdirectoryPaths;
+
+ RefreshView();
+ }
+
+ public void RefreshView()
+ {
+ lbxFolderLinks.Items.SortDescriptions.Add(new SortDescription("Path", ListSortDirection.Ascending));
+
+ lbxExcludedPaths.Items.SortDescriptions.Add(new SortDescription("Path", ListSortDirection.Ascending));
+
+ btnDelete.Visibility = Visibility.Hidden;
+ btnEdit.Visibility = Visibility.Hidden;
+ btnAdd.Visibility = Visibility.Hidden;
+
+ if (expFolderLinks.IsExpanded || expExcludedPaths.IsExpanded)
+ {
+ btnAdd.Visibility = Visibility.Visible;
+
+ if ((lbxFolderLinks.Items.Count == 0 && lbxExcludedPaths.Items.Count == 0)
+ && btnDelete.Visibility == Visibility.Visible
+ && btnEdit.Visibility == Visibility.Visible)
+ {
+ btnDelete.Visibility = Visibility.Hidden;
+ btnEdit.Visibility = Visibility.Hidden;
+ }
+
+ if (expFolderLinks.IsExpanded
+ && lbxFolderLinks.Items.Count > 0
+ && btnDelete.Visibility == Visibility.Hidden
+ && btnEdit.Visibility == Visibility.Hidden)
+ {
+ btnDelete.Visibility = Visibility.Visible;
+ btnEdit.Visibility = Visibility.Visible;
+ }
+
+ if (expExcludedPaths.IsExpanded
+ && lbxExcludedPaths.Items.Count > 0
+ && btnDelete.Visibility == Visibility.Hidden
+ && btnEdit.Visibility == Visibility.Hidden)
+ {
+ btnDelete.Visibility = Visibility.Visible;
+ btnEdit.Visibility = Visibility.Visible;
+ }
+ }
+
+ lbxFolderLinks.Items.Refresh();
+
+ lbxExcludedPaths.Items.Refresh();
+ }
+
+ private void expFolderLinks_Click(object sender, RoutedEventArgs e)
+ {
+ if (expFolderLinks.IsExpanded)
+ expFolderLinks.Height = 235;
+
+ if (!expFolderLinks.IsExpanded)
+ expFolderLinks.Height = Double.NaN;
+
+ if (expExcludedPaths.IsExpanded)
+ expExcludedPaths.IsExpanded = false;
+
+ RefreshView();
+ }
+
+ private void expExcludedPaths_Click(object sender, RoutedEventArgs e)
+ {
+ if (expExcludedPaths.IsExpanded)
+ expFolderLinks.Height = Double.NaN;
+
+ if (expFolderLinks.IsExpanded)
+ expFolderLinks.IsExpanded = false;
+
+ RefreshView();
+ }
+
+ private void btnDelete_Click(object sender, RoutedEventArgs e)
+ {
+ var selectedRow = lbxFolderLinks.SelectedItem as FolderLink?? lbxExcludedPaths.SelectedItem as FolderLink;
+
+ if (selectedRow != null)
+ {
+ string msg = string.Format(viewModel.Context.API.GetTranslation("plugin_explorer_delete_folder_link"), selectedRow.Path);
+
+ if (MessageBox.Show(msg, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.Yes)
+ {
+ if (expFolderLinks.IsExpanded)
+ viewModel.RemoveFolderLinkFromQuickFolders(selectedRow);
+
+ if (expExcludedPaths.IsExpanded)
+ viewModel.RemoveFolderLinkFromExcludedIndexPaths(selectedRow);
+
+ RefreshView();
+ }
+ }
+ else
+ {
+ string warning = viewModel.Context.API.GetTranslation("plugin_explorer_select_folder_link_warning");
+ MessageBox.Show(warning);
+ }
+ }
+
+ private void btnEdit_Click(object sender, RoutedEventArgs e)
+ {
+ var selectedRow = lbxFolderLinks.SelectedItem as FolderLink ?? lbxExcludedPaths.SelectedItem as FolderLink;
+
+ if (selectedRow != null)
+ {
+ var folderBrowserDialog = new FolderBrowserDialog();
+ folderBrowserDialog.SelectedPath = selectedRow.Path;
+ if (folderBrowserDialog.ShowDialog() == DialogResult.OK)
+ {
+ if (expFolderLinks.IsExpanded)
+ {
+ var link = viewModel.Settings.QuickFolderAccessLinks.First(x => x.Path == selectedRow.Path);
+ link.Path = folderBrowserDialog.SelectedPath;
+ }
+
+ if (expExcludedPaths.IsExpanded)
+ {
+ var link = viewModel.Settings.IndexSearchExcludedSubdirectoryPaths.First(x => x.Path == selectedRow.Path);
+ link.Path = folderBrowserDialog.SelectedPath;
+ }
+ }
+
+ RefreshView();
+ }
+ else
+ {
+ string warning = viewModel.Context.API.GetTranslation("plugin_explorer_select_folder_link_warning");
+ MessageBox.Show(warning);
+ }
+ }
+
+ private void btnAdd_Click(object sender, RoutedEventArgs e)
+ {
+ var folderBrowserDialog = new FolderBrowserDialog();
+ if (folderBrowserDialog.ShowDialog() == DialogResult.OK)
+ {
+ var newFolderLink = new FolderLink
+ {
+ Path = folderBrowserDialog.SelectedPath
+ };
+
+ AddFolderLink(newFolderLink);
+ }
+
+ RefreshView();
+ }
+
+ private void lbxFolders_Drop(object sender, DragEventArgs e)
+ {
+ string[] files = (string[])e.Data.GetData(DataFormats.FileDrop);
+
+ if (files != null && files.Count() > 0)
+ {
+ if (expFolderLinks.IsExpanded && viewModel.Settings.QuickFolderAccessLinks == null)
+ viewModel.Settings.QuickFolderAccessLinks = new List();
+
+ foreach (string s in files)
+ {
+ if (Directory.Exists(s))
+ {
+ var newFolderLink = new FolderLink
+ {
+ Path = s
+ };
+
+ AddFolderLink(newFolderLink);
+ }
+
+ RefreshView();
+ }
+ }
+ }
+
+ private void AddFolderLink(FolderLink newFolderLink)
+ {
+ if (expFolderLinks.IsExpanded
+ && !viewModel.Settings.QuickFolderAccessLinks.Any(x => x.Path == newFolderLink.Path))
+ {
+ if (viewModel.Settings.QuickFolderAccessLinks == null)
+ viewModel.Settings.QuickFolderAccessLinks = new List();
+
+ viewModel.Settings.QuickFolderAccessLinks.Add(newFolderLink);
+ }
+
+ if (expExcludedPaths.IsExpanded
+ && !viewModel.Settings.IndexSearchExcludedSubdirectoryPaths.Any(x => x.Path == newFolderLink.Path))
+ {
+ if (viewModel.Settings.IndexSearchExcludedSubdirectoryPaths == null)
+ viewModel.Settings.IndexSearchExcludedSubdirectoryPaths = new List();
+
+ viewModel.Settings.IndexSearchExcludedSubdirectoryPaths.Add(newFolderLink);
+ }
+ }
+
+ private void lbxFolders_DragEnter(object sender, DragEventArgs e)
+ {
+ if (e.Data.GetDataPresent(DataFormats.FileDrop))
+ {
+ e.Effects = DragDropEffects.Link;
+ }
+ else
+ {
+ e.Effects = DragDropEffects.None;
+ }
+ }
+
+ private void btnOpenIndexingOptions_Click(object sender, RoutedEventArgs e)
+ {
+ viewModel.OpenWindowsIndexingOptions();
+ }
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json b/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json
new file mode 100644
index 000000000..10b3edd96
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json
@@ -0,0 +1,12 @@
+{
+ "ID": "572be03c74c642baae319fc283e561a8",
+ "ActionKeyword": "*",
+ "Name": "Explorer",
+ "Description": "Search and manage files and folders. Explorer utilises Windows Index Search",
+ "Author": "Jeremy Wu",
+ "Version": "1.0.0",
+ "Language": "csharp",
+ "Website": "https://github.com/Flow-Launcher/Flow.Launcher",
+ "ExecuteFileName": "Flow.Launcher.Plugin.Explorer.dll",
+ "IcoPath": "Images\\explorer.png"
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Folder/ContextMenuLoader.cs b/Plugins/Flow.Launcher.Plugin.Folder/ContextMenuLoader.cs
deleted file mode 100644
index 561047932..000000000
--- a/Plugins/Flow.Launcher.Plugin.Folder/ContextMenuLoader.cs
+++ /dev/null
@@ -1,219 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Threading.Tasks;
-using System.Windows;
-using Flow.Launcher.Infrastructure.Logger;
-using Flow.Launcher.Infrastructure.Image;
-using Flow.Launcher.Plugin.SharedCommands;
-
-namespace Flow.Launcher.Plugin.Folder
-{
- internal class ContextMenuLoader : IContextMenu
- {
- private readonly PluginInitContext _context;
-
- public ContextMenuLoader(PluginInitContext context)
- {
- _context = context;
- }
-
- public List LoadContextMenus(Result selectedResult)
- {
- var contextMenus = new List();
- if (selectedResult.ContextData is SearchResult record)
- {
- if (record.Type == ResultType.File)
- {
- contextMenus.Add(CreateOpenWithEditorResult(record));
- contextMenus.Add(CreateOpenContainingFolderResult(record));
- }
-
- var icoPath = (record.Type == ResultType.File) ? Main.FileImagePath : Main.FolderImagePath;
- var fileOrFolder = (record.Type == ResultType.File) ? "file" : "folder";
- contextMenus.Add(new Result
- {
- Title = "Copy path",
- SubTitle = $"Copy the current {fileOrFolder} path to clipboard",
- Action = (context) =>
- {
- try
- {
- Clipboard.SetText(record.FullPath);
- return true;
- }
- catch (Exception e)
- {
- var message = "Fail to set text in clipboard";
- LogException(message, e);
- _context.API.ShowMsg(message);
- return false;
- }
- },
- IcoPath = Main.CopyImagePath
- });
-
- contextMenus.Add(new Result
- {
- Title = $"Copy {fileOrFolder}",
- SubTitle = $"Copy the {fileOrFolder} to clipboard",
- Action = (context) =>
- {
- try
- {
- Clipboard.SetFileDropList(new System.Collections.Specialized.StringCollection { record.FullPath });
- return true;
- }
- catch (Exception e)
- {
- var message = $"Fail to set {fileOrFolder} in clipboard";
- LogException(message, e);
- _context.API.ShowMsg(message);
- return false;
- }
-
- },
- IcoPath = icoPath
- });
-
- if (record.Type == ResultType.File || record.Type == ResultType.Folder)
- contextMenus.Add(new Result
- {
- Title = $"Delete {fileOrFolder}",
- SubTitle = $"Delete the selected {fileOrFolder}",
- Action = (context) =>
- {
- try
- {
- if (record.Type == ResultType.File)
- File.Delete(record.FullPath);
- else
- Directory.Delete(record.FullPath, true);
- }
- catch(Exception e)
- {
- var message = $"Fail to delete {fileOrFolder} at {record.FullPath}";
- LogException(message, e);
- _context.API.ShowMsg(message);
- return false;
- }
-
- return true;
- },
- IcoPath = Main.DeleteFileFolderImagePath
- });
-
- if (record.Type == ResultType.File && CanRunAsDifferentUser(record.FullPath))
- contextMenus.Add(new Result
- {
- Title = "Run as different user",
- Action = (context) =>
- {
- try
- {
- Task.Run(()=> ShellCommand.RunAsDifferentUser(record.FullPath.SetProcessStartInfo()));
- }
- catch (FileNotFoundException e)
- {
- var name = "Plugin: Folder";
- var message = $"File not found: {e.Message}";
- _context.API.ShowMsg(name, message);
- }
-
- return true;
- },
- IcoPath = "Images/user.png"
- });
- }
-
- return contextMenus;
- }
-
- private Result CreateOpenContainingFolderResult(SearchResult record)
- {
- return new Result
- {
- Title = "Open containing folder",
- Action = _ =>
- {
- try
- {
- Process.Start("explorer.exe", $" /select,\"{record.FullPath}\"");
- }
- catch(Exception e)
- {
- var message = $"Fail to open file at {record.FullPath}";
- LogException(message, e);
- _context.API.ShowMsg(message);
- return false;
- }
-
- return true;
- },
- IcoPath = Main.FolderImagePath
- };
- }
-
-
- private Result CreateOpenWithEditorResult(SearchResult record)
- {
- string editorPath = "notepad.exe"; // TODO add the ability to create a custom editor
-
- var name = "Open With Editor: " + Path.GetFileNameWithoutExtension(editorPath);
- return new Result
- {
- Title = name,
- Action = _ =>
- {
- try
- {
- Process.Start(editorPath, record.FullPath);
- return true;
- }
- catch (Exception e)
- {
- var message = $"Fail to editor for file at {record.FullPath}";
- LogException(message, e);
- _context.API.ShowMsg(message);
- return false;
- }
- },
- IcoPath = editorPath
- };
- }
-
- public void LogException(string message, Exception e)
- {
- Log.Exception($"|Flow.Launcher.Plugin.Folder.ContextMenu|{message}", e);
- }
-
- private bool CanRunAsDifferentUser(string path)
- {
- switch(Path.GetExtension(path))
- {
- case ".exe":
- case ".bat":
- case ".msi":
- return true;
-
- default:
- return false;
-
- }
- }
- }
-
- public class SearchResult
- {
- public string FullPath { get; set; }
- public ResultType Type { get; set; }
- }
-
- public enum ResultType
- {
- Volume,
- Folder,
- File
- }
-}
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Folder/FolderLink.cs b/Plugins/Flow.Launcher.Plugin.Folder/FolderLink.cs
deleted file mode 100644
index b7bcb1d34..000000000
--- a/Plugins/Flow.Launcher.Plugin.Folder/FolderLink.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using System;
-using System.Linq;
-using Newtonsoft.Json;
-
-namespace Flow.Launcher.Plugin.Folder
-{
- [JsonObject(MemberSerialization.OptIn)]
- public class FolderLink
- {
- [JsonProperty]
- public string Path { get; set; }
-
- public string Nickname =>
- Path.Split(new[] { System.IO.Path.DirectorySeparatorChar }, StringSplitOptions.None)
- .Last()
- + " (" + System.IO.Path.GetDirectoryName(Path) + ")";
- }
-}
diff --git a/Plugins/Flow.Launcher.Plugin.Folder/FolderPluginSettings.xaml b/Plugins/Flow.Launcher.Plugin.Folder/FolderPluginSettings.xaml
deleted file mode 100644
index fb43d6b7c..000000000
--- a/Plugins/Flow.Launcher.Plugin.Folder/FolderPluginSettings.xaml
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Plugins/Flow.Launcher.Plugin.Folder/FolderPluginSettings.xaml.cs b/Plugins/Flow.Launcher.Plugin.Folder/FolderPluginSettings.xaml.cs
deleted file mode 100644
index c95f70ef9..000000000
--- a/Plugins/Flow.Launcher.Plugin.Folder/FolderPluginSettings.xaml.cs
+++ /dev/null
@@ -1,156 +0,0 @@
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.IO;
-using System.Linq;
-using System.Windows;
-using System.Windows.Forms;
-using DataFormats = System.Windows.DataFormats;
-using DragDropEffects = System.Windows.DragDropEffects;
-using DragEventArgs = System.Windows.DragEventArgs;
-using MessageBox = System.Windows.MessageBox;
-
-namespace Flow.Launcher.Plugin.Folder
-{
-
- public partial class FolderPluginSettings
- {
- private IPublicAPI flowlauncherAPI;
- private Settings _settings;
-
- public FolderPluginSettings(IPublicAPI flowlauncherAPI, Settings settings)
- {
- this.flowlauncherAPI = flowlauncherAPI;
- InitializeComponent();
- _settings = settings;
- lbxFolders.ItemsSource = _settings.FolderLinks;
-
- RefreshView();
- }
-
- public void RefreshView()
- {
- lbxFolders.Items.SortDescriptions.Add(new SortDescription("Path", ListSortDirection.Ascending));
-
- if (lbxFolders.Items.Count == 0
- && btnDelete.Visibility == Visibility.Visible
- && btnEdit.Visibility == Visibility.Visible)
- {
- btnDelete.Visibility = Visibility.Hidden;
- btnEdit.Visibility = Visibility.Hidden;
- }
-
- if (lbxFolders.Items.Count > 0
- && btnDelete.Visibility == Visibility.Hidden
- && btnEdit.Visibility == Visibility.Hidden)
- {
- btnDelete.Visibility = Visibility.Visible;
- btnEdit.Visibility = Visibility.Visible;
- }
-
- lbxFolders.Items.Refresh();
- }
-
- private void btnDelete_Click(object sender, RoutedEventArgs e)
- {
- var selectedFolder = lbxFolders.SelectedItem as FolderLink;
- if (selectedFolder != null)
- {
- string msg = string.Format(flowlauncherAPI.GetTranslation("flowlauncher_plugin_folder_delete_folder_link"), selectedFolder.Path);
-
- if (MessageBox.Show(msg, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.Yes)
- {
- _settings.FolderLinks.Remove(selectedFolder);
- RefreshView();
- }
- }
- else
- {
- string warning = flowlauncherAPI.GetTranslation("flowlauncher_plugin_folder_select_folder_link_warning");
- MessageBox.Show(warning);
- }
- }
-
- private void btnEdit_Click(object sender, RoutedEventArgs e)
- {
- var selectedFolder = lbxFolders.SelectedItem as FolderLink;
- if (selectedFolder != null)
- {
- var folderBrowserDialog = new FolderBrowserDialog();
- folderBrowserDialog.SelectedPath = selectedFolder.Path;
- if (folderBrowserDialog.ShowDialog() == DialogResult.OK)
- {
- var link = _settings.FolderLinks.First(x => x.Path == selectedFolder.Path);
- link.Path = folderBrowserDialog.SelectedPath;
- }
-
- RefreshView();
- }
- else
- {
- string warning = flowlauncherAPI.GetTranslation("flowlauncher_plugin_folder_select_folder_link_warning");
- MessageBox.Show(warning);
- }
- }
-
- private void btnAdd_Click(object sender, RoutedEventArgs e)
- {
- var folderBrowserDialog = new FolderBrowserDialog();
- if (folderBrowserDialog.ShowDialog() == DialogResult.OK)
- {
- var newFolder = new FolderLink
- {
- Path = folderBrowserDialog.SelectedPath
- };
-
- if (_settings.FolderLinks == null)
- {
- _settings.FolderLinks = new List();
- }
-
- _settings.FolderLinks.Add(newFolder);
- }
-
- RefreshView();
- }
-
- private void lbxFolders_Drop(object sender, DragEventArgs e)
- {
- string[] files = (string[])e.Data.GetData(DataFormats.FileDrop);
-
- if (files != null && files.Count() > 0)
- {
- if (_settings.FolderLinks == null)
- {
- _settings.FolderLinks = new List();
- }
-
- foreach (string s in files)
- {
- if (Directory.Exists(s))
- {
- var newFolder = new FolderLink
- {
- Path = s
- };
-
- _settings.FolderLinks.Add(newFolder);
- }
-
- RefreshView();
- }
- }
- }
-
- private void lbxFolders_DragEnter(object sender, DragEventArgs e)
- {
- if (e.Data.GetDataPresent(DataFormats.FileDrop))
- {
- e.Effects = DragDropEffects.Link;
- }
- else
- {
- e.Effects = DragDropEffects.None;
- }
- }
- }
-}
diff --git a/Plugins/Flow.Launcher.Plugin.Folder/Languages/de.xaml b/Plugins/Flow.Launcher.Plugin.Folder/Languages/de.xaml
deleted file mode 100644
index b39e756b6..000000000
--- a/Plugins/Flow.Launcher.Plugin.Folder/Languages/de.xaml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
- Löschen
- Bearbeiten
- Hinzufügen
- Ordnerpfad
- Bitte wähle eine Ordnerverknüpfung
- Bist du sicher {0} zu löschen?
-
- Ordner
- Öffne deine Lieblingsordner direkt von Flow Launcher aus
-
-
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Folder/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Folder/Languages/en.xaml
deleted file mode 100644
index 9ee62927d..000000000
--- a/Plugins/Flow.Launcher.Plugin.Folder/Languages/en.xaml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
- Delete
- Edit
- Add
- Folder Path
- Please select a folder link
- Are you sure you want to delete {0}?
-
- Folder
- Open favorite folder from Flow Launcher directly
-
-
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Folder/Languages/pl.xaml b/Plugins/Flow.Launcher.Plugin.Folder/Languages/pl.xaml
deleted file mode 100644
index 440da079e..000000000
--- a/Plugins/Flow.Launcher.Plugin.Folder/Languages/pl.xaml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
- Usuń
- Edytuj
- Dodaj
- Ścieżka folderu
- Musisz wybrać któryś folder z listy
- Czy jesteś pewien że chcesz usunąć {0}?
-
- Foldery
- Otwórz ulubione foldery bezpośrednio z poziomu Flow Launchera
-
-
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Folder/Languages/tr.xaml b/Plugins/Flow.Launcher.Plugin.Folder/Languages/tr.xaml
deleted file mode 100644
index f9d4beb1e..000000000
--- a/Plugins/Flow.Launcher.Plugin.Folder/Languages/tr.xaml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
- Sil
- Düzenle
- Ekle
- Klasör Konumu
- Lütfen bir klasör bağlantısı seçin
- {0} bağlantısını silmek istediğinize emin misiniz?
-
- Klasör
- Favori klasörünüzü doğrudan Flow Launcher'tan açın
-
-
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Folder/Languages/zh-cn.xaml b/Plugins/Flow.Launcher.Plugin.Folder/Languages/zh-cn.xaml
deleted file mode 100644
index 434728720..000000000
--- a/Plugins/Flow.Launcher.Plugin.Folder/Languages/zh-cn.xaml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
- 删除
- 编辑
- 添加
- 文件夹路径
- 请选择一个文件夹
- 你确定要删除{0}吗?
-
- 文件夹
- 在Flow Launcher中直接打开收藏的文件夹
-
-
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Folder/Languages/zh-tw.xaml b/Plugins/Flow.Launcher.Plugin.Folder/Languages/zh-tw.xaml
deleted file mode 100644
index 22a08f56a..000000000
--- a/Plugins/Flow.Launcher.Plugin.Folder/Languages/zh-tw.xaml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
- 刪除
- 編輯
- 新增
- 資料夾路徑
- 請選擇一個資料夾
- 你確認要刪除{0}嗎?
-
- 資料夾
- 在 Flow Launcher 中直接開啟收藏的資料夾
-
-
diff --git a/Plugins/Flow.Launcher.Plugin.Folder/Main.cs b/Plugins/Flow.Launcher.Plugin.Folder/Main.cs
deleted file mode 100644
index dd85dfd18..000000000
--- a/Plugins/Flow.Launcher.Plugin.Folder/Main.cs
+++ /dev/null
@@ -1,396 +0,0 @@
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Windows;
-using System.Windows.Controls;
-using Flow.Launcher.Infrastructure;
-using Flow.Launcher.Infrastructure.Storage;
-using Flow.Launcher.Plugin.SharedCommands;
-
-namespace Flow.Launcher.Plugin.Folder
-{
- public class Main : IPlugin, ISettingProvider, IPluginI18n, ISavable, IContextMenu
- {
- public const string FolderImagePath = "Images\\folder.png";
- public const string FileImagePath = "Images\\file.png";
- public const string DeleteFileFolderImagePath = "Images\\deletefilefolder.png";
- public const string CopyImagePath = "Images\\copy.png";
-
- private string DefaultFolderSubtitleString = "Ctrl + Enter to open the directory";
-
- private static List _driverNames;
- private PluginInitContext _context;
-
- private readonly Settings _settings;
- private readonly PluginJsonStorage _storage;
- private IContextMenu _contextMenuLoader;
-
- private static Dictionary _envStringPaths;
-
- public Main()
- {
- _storage = new PluginJsonStorage();
- _settings = _storage.Load();
- }
-
- public void Save()
- {
- _storage.Save();
- }
-
- public Control CreateSettingPanel()
- {
- return new FolderPluginSettings(_context.API, _settings);
- }
-
- public void Init(PluginInitContext context)
- {
- _context = context;
- _contextMenuLoader = new ContextMenuLoader(context);
- InitialDriverList();
- LoadEnvironmentStringPaths();
- }
-
- public List Query(Query query)
- {
- var results = GetUserFolderResults(query);
-
- string search = query.Search.ToLower();
- if (!IsDriveOrSharedFolder(search) && !IsEnvironmentVariableSearch(search))
- {
- return results;
- }
-
- if (IsEnvironmentVariableSearch(search))
- {
- results.AddRange(GetEnvironmentStringPathResults(search, query));
- }
- else
- {
- results.AddRange(QueryInternal_Directory_Exists(query.Search, query));
- }
-
- // todo why was this hack here?
- foreach (var result in results)
- {
- result.Score += 10;
- }
-
- return results;
- }
-
- private static bool IsEnvironmentVariableSearch(string search)
- {
- return _envStringPaths != null && search.StartsWith("%");
- }
-
- private static bool IsDriveOrSharedFolder(string search)
- {
- if (search.StartsWith(@"\\"))
- { // shared folder
- return true;
- }
-
- if (_driverNames != null && _driverNames.Any(search.StartsWith))
- { // normal drive letter
- return true;
- }
-
- if (_driverNames == null && search.Length > 2 && char.IsLetter(search[0]) && search[1] == ':')
- { // when we don't have the drive letters we can try...
- return true; // we don't know so let's give it the possibility
- }
-
- return false;
- }
-
- private Result CreateFolderResult(string title, string subtitle, string path, Query query)
- {
- return new Result
- {
- Title = title,
- IcoPath = path,
- SubTitle = subtitle,
- TitleHighlightData = StringMatcher.FuzzySearch(query.Search, title).MatchData,
- Action = c =>
- {
- if (c.SpecialKeyState.CtrlPressed)
- {
- try
- {
- FilesFolders.OpenPath(path);
- return true;
- }
- catch (Exception ex)
- {
- MessageBox.Show(ex.Message, "Could not start " + path);
- return false;
- }
- }
-
- string changeTo = path.EndsWith("\\") ? path : path + "\\";
- _context.API.ChangeQuery(string.IsNullOrEmpty(query.ActionKeyword) ?
- changeTo :
- query.ActionKeyword + " " + changeTo);
- return false;
- },
- ContextData = new SearchResult { Type = ResultType.Folder, FullPath = path }
- };
- }
-
- private List GetUserFolderResults(Query query)
- {
- string search = query.Search.ToLower();
- var userFolderLinks = _settings.FolderLinks.Where(
- x => x.Nickname.StartsWith(search, StringComparison.OrdinalIgnoreCase));
- var results = userFolderLinks.Select(item =>
- CreateFolderResult(item.Nickname, DefaultFolderSubtitleString, item.Path, query)).ToList();
- return results;
- }
-
- private void InitialDriverList()
- {
- if (_driverNames == null)
- {
- _driverNames = new List();
- var allDrives = DriveInfo.GetDrives();
- foreach (DriveInfo driver in allDrives)
- {
- _driverNames.Add(driver.Name.ToLower().TrimEnd('\\'));
- }
- }
- }
-
- private void LoadEnvironmentStringPaths()
- {
- _envStringPaths = new Dictionary();
-
- foreach (DictionaryEntry special in Environment.GetEnvironmentVariables())
- {
- if (Directory.Exists(special.Value.ToString()))
- {
- _envStringPaths.Add(special.Key.ToString().ToLower(), special.Value.ToString());
- }
- }
- }
-
- private static readonly char[] _specialSearchChars = new char[]
- {
- '?', '*', '>'
- };
-
- private List QueryInternal_Directory_Exists(string search, Query query)
- {
- var results = new List();
- var hasSpecial = search.IndexOfAny(_specialSearchChars) >= 0;
- string incompleteName = "";
- if (hasSpecial || !Directory.Exists(search + "\\"))
- {
- // if folder doesn't exist, we want to take the last part and use it afterwards to help the user
- // find the right folder.
- int index = search.LastIndexOf('\\');
- if (index > 0 && index < (search.Length - 1))
- {
- incompleteName = search.Substring(index + 1).ToLower();
- search = search.Substring(0, index + 1);
- if (!Directory.Exists(search))
- {
- return results;
- }
- }
- else
- {
- return results;
- }
- }
- else
- {
- // folder exist, add \ at the end of doesn't exist
- if (!search.EndsWith("\\"))
- {
- search += "\\";
- }
- }
-
- results.Add(CreateOpenCurrentFolderResult(incompleteName, search));
-
- var searchOption = SearchOption.TopDirectoryOnly;
- incompleteName += "*";
-
- // give the ability to search all folder when starting with >
- if (incompleteName.StartsWith(">"))
- {
- searchOption = SearchOption.AllDirectories;
-
- // match everything before and after search term using supported wildcard '*', ie. *searchterm*
- incompleteName = "*" + incompleteName.Substring(1);
- }
-
- var folderList = new List();
- var fileList = new List();
-
- var folderSubtitleString = DefaultFolderSubtitleString;
-
- try
- {
- // search folder and add results
- var directoryInfo = new DirectoryInfo(search);
- var fileSystemInfos = directoryInfo.GetFileSystemInfos(incompleteName, searchOption);
-
- foreach (var fileSystemInfo in fileSystemInfos)
- {
- if ((fileSystemInfo.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) continue;
-
- if(fileSystemInfo is DirectoryInfo)
- {
- if (searchOption == SearchOption.AllDirectories)
- folderSubtitleString = fileSystemInfo.FullName;
-
- folderList.Add(CreateFolderResult(fileSystemInfo.Name, folderSubtitleString, fileSystemInfo.FullName, query));
- }
- else
- {
- fileList.Add(CreateFileResult(fileSystemInfo.FullName, query));
- }
- }
- }
- catch (Exception e)
- {
- if (e is UnauthorizedAccessException || e is ArgumentException)
- {
- results.Add(new Result { Title = e.Message, Score = 501 });
-
- return results;
- }
-
- throw;
- }
-
- // Intial ordering, this order can be updated later by UpdateResultView.MainViewModel based on history of user selection.
- return results.Concat(folderList.OrderBy(x => x.Title)).Concat(fileList.OrderBy(x => x.Title)).ToList();
- }
-
- private List GetEnvironmentStringPathSuggestions(string search, Query query)
- {
- var results = new List();
- foreach (var p in _envStringPaths)
- {
- if (p.Key.StartsWith(search))
- {
- results.Add(CreateFolderResult($"%{p.Key}%", p.Value, p.Value, query));
- }
- }
- return results;
- }
-
- private List GetEnvironmentStringPathResults(string envStringSearch, Query query)
- {
- if (envStringSearch == "%")
- { // return all environment string options as path suggestions
- return GetEnvironmentStringPathSuggestions("", query);
- }
-
- var results = new List();
- var search = envStringSearch.Substring(1);
-
- if (search.EndsWith("%") && search.Length > 1)
- { // query starts and ends with a %, find an exact match from env-string paths
- var exactEnvStringPath = search.Substring(0, search.Length-1);
-
- if (_envStringPaths.ContainsKey(exactEnvStringPath))
- {
- var expandedPath = _envStringPaths[exactEnvStringPath];
- results.Add(CreateFolderResult($"%{exactEnvStringPath}%", expandedPath, expandedPath, query));
- }
- }
- else if (search.Contains("%"))
- { // query starts with a % and contains another % somewhere before the end
- var splitSearch = search.Split("%");
- var exactEnvStringPath = splitSearch[0];
-
- // if there are more than 2 % characters in the query, don't bother
- if (splitSearch.Length == 2 && _envStringPaths.ContainsKey(exactEnvStringPath))
- {
- var queryPartToReplace = $"%{exactEnvStringPath}%";
- var expandedPath = _envStringPaths[exactEnvStringPath];
- // replace the %envstring% part of the query with its expanded equivalent
- var updatedSearch = envStringSearch.Replace(queryPartToReplace, expandedPath);
-
- results.AddRange(QueryInternal_Directory_Exists(updatedSearch, query));
- }
- }
- else
- { // query simply starts wtih a %, suggest env-string paths that match the rest of the search
- results.AddRange(GetEnvironmentStringPathSuggestions(search, query));
- }
-
- return results;
- }
-
- private static Result CreateFileResult(string filePath, Query query)
- {
- var result = new Result
- {
- Title = Path.GetFileName(filePath),
- SubTitle = filePath,
- IcoPath = filePath,
- TitleHighlightData = StringMatcher.FuzzySearch(query.Search, Path.GetFileName(filePath)).MatchData,
- Action = c =>
- {
- try
- {
- FilesFolders.OpenPath(filePath);
- }
- catch (Exception ex)
- {
- MessageBox.Show(ex.Message, "Could not start " + filePath);
- }
-
- return true;
- },
- ContextData = new SearchResult { Type = ResultType.File, FullPath = filePath}
- };
- return result;
- }
-
- private static Result CreateOpenCurrentFolderResult(string incompleteName, string search)
- {
- var firstResult = "Open current directory";
- if (incompleteName.Length > 0)
- firstResult = "Open " + search;
-
- var folderName = search.TrimEnd('\\').Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.None).Last();
-
- return new Result
- {
- Title = firstResult,
- SubTitle = $"Use > to search files and subfolders within {folderName}, " +
- $"* to search for file extensions in {folderName} or both >* to combine the search",
- IcoPath = search,
- Score = 500,
- Action = c =>
- {
- FilesFolders.OpenPath(search);
- return true;
- }
- };
- }
-
- public string GetTranslatedPluginTitle()
- {
- return _context.API.GetTranslation("flowlauncher_plugin_folder_plugin_name");
- }
-
- public string GetTranslatedPluginDescription()
- {
- return _context.API.GetTranslation("flowlauncher_plugin_folder_plugin_description");
- }
-
- public List LoadContextMenus(Result selectedResult)
- {
- return _contextMenuLoader.LoadContextMenus(selectedResult);
- }
- }
-}
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Folder/Settings.cs b/Plugins/Flow.Launcher.Plugin.Folder/Settings.cs
deleted file mode 100644
index b605dfda4..000000000
--- a/Plugins/Flow.Launcher.Plugin.Folder/Settings.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using System.Collections.Generic;
-using Newtonsoft.Json;
-using Flow.Launcher.Infrastructure.Storage;
-
-namespace Flow.Launcher.Plugin.Folder
-{
- public class Settings
- {
- [JsonProperty]
- public List FolderLinks { get; set; } = new List();
- }
-}
diff --git a/Plugins/Flow.Launcher.Plugin.Folder/plugin.json b/Plugins/Flow.Launcher.Plugin.Folder/plugin.json
deleted file mode 100644
index 87a444791..000000000
--- a/Plugins/Flow.Launcher.Plugin.Folder/plugin.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "ID": "B4D3B69656E14D44865C8D818EAE47C4",
- "ActionKeyword": "*",
- "Name": "Folder",
- "Description": "Open favorite folder from Flow Launcher directorily",
- "Author": "qianlifeng",
- "Version": "1.0.0",
- "Language": "csharp",
- "Website": "https://github.com/Flow-Launcher/Flow.Launcher",
- "ExecuteFileName": "Flow.Launcher.Plugin.Folder.dll",
- "IcoPath": "Images\\folder.png"
-}
diff --git a/README.md b/README.md
index 387e98b6f..23dde0a1d 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ Flow Launcher. Dedicated to make your workflow flow more seamlessly. Aimed at be
## Features

-- Search everything from applications, folders, bookmarks, YouTube, Twitter and more. All from the comfort of your keyboard without ever touching the mouse.
+- Search everything from applications, files, bookmarks, YouTube, Twitter and more. All from the comfort of your keyboard without ever touching the mouse.
- Run batch and PowerShell commands as Administrator or a different user.
- Support languages from Chinese to Italian and more.
- Support of wide range of plugins.
diff --git a/appveyor.yml b/appveyor.yml
index e476bd3b1..c44372f9b 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -4,6 +4,8 @@ init:
- ps: |
$version = new-object System.Version $env:APPVEYOR_BUILD_VERSION
$env:flowVersion = "{0}.{1}.{2}" -f $version.Major, $version.Minor, $version.Build
+- sc config WSearch start= auto # Starts Windows Search service- Needed for running ExplorerTest
+- net start WSearch
assembly_info:
patch: true