From f77257997d725f72e9dc888a7e96496ae341e158 Mon Sep 17 00:00:00 2001 From: iSazonov Date: Tue, 29 Aug 2017 18:10:01 +0300 Subject: [PATCH 1/4] Make TabCompletion case-insensivite for file names on Unix --- .../CommandCompletion/CompletionCompleters.cs | 16 ++++++++++++++++ .../Host/TabCompletion/TabCompletion.Tests.ps1 | 17 ++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 39a9860df95..8719dce86d0 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -4072,7 +4072,23 @@ internal static IEnumerable CompleteFilename(CompletionContext // can succeed. This call will mount the drive if it wasn't already. executionContext.SessionState.Drive.GetAtScope(wordToComplete.Substring(0, 1), "global"); } +#if UNIX + StringBuilder sb = new StringBuilder(); + + foreach (var ch in wordToComplete.ToCharArray()) + { + if (Char.IsLetter(ch)) + { + sb.Append('[').Append(Char.ToLower(ch)).Append(Char.ToUpper(ch)).Append(']'); + } + else + { + sb.Append(ch); + } + } + wordToComplete = sb.ToString(); +#endif var powerShellExecutionHelper = context.Helper; powerShellExecutionHelper .AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Resolve-Path") diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index b199918dd7b..a384670d8a6 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -197,8 +197,12 @@ Describe "TabCompletion" -Tags CI { Context "File name completion" { BeforeAll { + $oneSubDirLowerTest = "onesubd" + $oneSubDirUpperTest = "oNeSubD" + $oneSubDirTest = "oneSubDir" + $tempDir = Join-Path -Path $TestDrive -ChildPath "baseDir" - $oneSubDir = Join-Path -Path $tempDir -ChildPath "oneSubDir" + $oneSubDir = Join-Path -Path $tempDir -ChildPath $oneSubDirTest $oneSubDirPrime = Join-Path -Path $tempDir -ChildPath "prime" $twoSubDir = Join-Path -Path $oneSubDir -ChildPath "twoSubDir" @@ -241,6 +245,17 @@ Describe "TabCompletion" -Tags CI { Pop-Location } + It "TabCompletion should be case-insensitive for file names" { + Push-Location -Path $tempDir + $res = TabExpansion2 -inputScript $oneSubDirLowerTest -cursorColumn $oneSubDirLowerTest.Length + $res.CompletionMatches.Count | Should BeGreaterThan 0 + $res.CompletionMatches[0].CompletionText | Should Be ".${separator}$oneSubDirTest" + + $res = TabExpansion2 -inputScript $oneSubDirUpperTest -cursorColumn $oneSubDirUpperTest.Length + $res.CompletionMatches.Count | Should BeGreaterThan 0 + $res.CompletionMatches[0].CompletionText | Should Be ".${separator}$oneSubDirTest" + } + It "Input '' should successfully complete" -TestCases $testCases { param ($inputStr, $localExpected) From aba45ac665ee5f86dd6efce4372c3ddde5592ae0 Mon Sep 17 00:00:00 2001 From: iSazonov Date: Wed, 30 Aug 2017 11:51:22 +0300 Subject: [PATCH 2/4] Address feedbacks --- .../CommandCompletion/CompletionCompleters.cs | 38 ++++++++++++++----- .../engine/Utils.cs | 1 + 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 8719dce86d0..6fa80b34662 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -4073,21 +4073,41 @@ internal static IEnumerable CompleteFilename(CompletionContext executionContext.SessionState.Drive.GetAtScope(wordToComplete.Substring(0, 1), "global"); } #if UNIX - StringBuilder sb = new StringBuilder(); - - foreach (var ch in wordToComplete.ToCharArray()) + // We use globbing to get completions. + // Globbing is based on system functions which is case-sensitive on Unix. + // To make globbing on Unix case-insensitive we transform every char from the input string: + // "alias:dir" -> "alias:[dD][iI][rR]" + if (context.GetOption("UnixCaseInsensitiveGlobbing", @default: true) == true) { - if (Char.IsLetter(ch)) + StringBuilder sb = new StringBuilder(wordToComplete.Length * 4); + + //wordToComplete = WildcardPattern.Escape(wordToComplete, Utils.Separators.Brackets); + + // Copy a provider name "as is" without transformation. + var providerPrefixIndex = wordToComplete.IndexOf(':') + 1; + int i; + for (i=0; i < providerPrefixIndex; i++) { - sb.Append('[').Append(Char.ToLower(ch)).Append(Char.ToUpper(ch)).Append(']'); + sb.Append(wordToComplete[i]); } - else + + // Transform rest of input string after a provider prefix. + for (var j=i; j < wordToComplete.Length; j++) { - sb.Append(ch); + var lch = Char.ToLower(wordToComplete[j]); + var uch = Char.ToUpper(wordToComplete[j]); + if (lch != uch) + { + sb.Append('[').Append(lch).Append(uch).Append(']'); + } + else + { + sb.Append(wordToComplete[j]); + } } - } - wordToComplete = sb.ToString(); + wordToComplete = sb.ToString(); + } #endif var powerShellExecutionHelper = context.Helper; powerShellExecutionHelper diff --git a/src/System.Management.Automation/engine/Utils.cs b/src/System.Management.Automation/engine/Utils.cs index 11798694ea0..490c485b83d 100644 --- a/src/System.Management.Automation/engine/Utils.cs +++ b/src/System.Management.Automation/engine/Utils.cs @@ -1356,6 +1356,7 @@ internal static class Separators internal static readonly char[] SpaceOrTab = new char[] { ' ', '\t' }; internal static readonly char[] Newline = new char[] { '\n' }; internal static readonly char[] CrLf = new char[] { '\r', '\n' }; + internal static readonly char[] Brackets = new char[] { '[', ']' }; // (Copied from System.IO.Path so we can call TrimEnd in the same way that Directory.EnumerateFiles would on the search patterns). // Trim trailing white spaces, tabs etc but don't be aggressive in removing everything that has UnicodeCategory of trailing space. From 0f44e3452f5b31d908e5b7a7edf87748fd5b927d Mon Sep 17 00:00:00 2001 From: iSazonov Date: Wed, 30 Aug 2017 16:31:25 +0300 Subject: [PATCH 3/4] Fix test --- .../engine/CommandCompletion/CompletionCompleters.cs | 7 +++---- src/System.Management.Automation/engine/Utils.cs | 1 - test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 6fa80b34662..dc9b0af45fe 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -4061,10 +4061,8 @@ internal static IEnumerable CompleteFilename(CompletionContext var relativePaths = context.GetOption("RelativePaths", @default: defaultRelative); var useLiteralPath = context.GetOption("LiteralPaths", @default: false); - if (useLiteralPath && LocationGlobber.StringContainsGlobCharacters(wordToComplete)) - { - wordToComplete = WildcardPattern.Escape(wordToComplete, Utils.Separators.StarOrQuestion); - } + // We should always escape '[' and ']' if present. + wordToComplete = WildcardPattern.Escape(wordToComplete, Utils.Separators.StarOrQuestion); if (!defaultRelative && wordToComplete.Length >= 2 && wordToComplete[1] == ':' && char.IsLetter(wordToComplete[0]) && executionContext != null) { @@ -4072,6 +4070,7 @@ internal static IEnumerable CompleteFilename(CompletionContext // can succeed. This call will mount the drive if it wasn't already. executionContext.SessionState.Drive.GetAtScope(wordToComplete.Substring(0, 1), "global"); } + #if UNIX // We use globbing to get completions. // Globbing is based on system functions which is case-sensitive on Unix. diff --git a/src/System.Management.Automation/engine/Utils.cs b/src/System.Management.Automation/engine/Utils.cs index 490c485b83d..11798694ea0 100644 --- a/src/System.Management.Automation/engine/Utils.cs +++ b/src/System.Management.Automation/engine/Utils.cs @@ -1356,7 +1356,6 @@ internal static class Separators internal static readonly char[] SpaceOrTab = new char[] { ' ', '\t' }; internal static readonly char[] Newline = new char[] { '\n' }; internal static readonly char[] CrLf = new char[] { '\r', '\n' }; - internal static readonly char[] Brackets = new char[] { '[', ']' }; // (Copied from System.IO.Path so we can call TrimEnd in the same way that Directory.EnumerateFiles would on the search patterns). // Trim trailing white spaces, tabs etc but don't be aggressive in removing everything that has UnicodeCategory of trailing space. diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index a384670d8a6..192b32685c1 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -493,8 +493,8 @@ Describe "TabCompletion" -Tags CI { $beforeTab = 'filesystem::{0}\Wind' -f $env:SystemDrive $afterTab = 'filesystem::{0}\Windows' -f $env:SystemDrive } else { - $beforeTab = 'filesystem::/us' -f $env:SystemDrive - $afterTab = 'filesystem::/usr' -f $env:SystemDrive + $beforeTab = 'filesystem::/sbi' -f $env:SystemDrive + $afterTab = 'filesystem::/sbin' -f $env:SystemDrive } $res = TabExpansion2 -inputScript $beforeTab -cursorColumn $beforeTab.Length $res.CompletionMatches.Count | Should BeGreaterThan 0 From fb1eb1b773c1420182aa2894708e4b2827650902 Mon Sep 17 00:00:00 2001 From: iSazonov Date: Fri, 8 Sep 2017 16:39:54 +0300 Subject: [PATCH 4/4] Add case-sensitive sorting for Unix --- .../CommandCompletion/CompletionCompleters.cs | 25 +++++++++++++++++-- .../TabCompletion/TabCompletion.Tests.ps1 | 24 +++++++++++++++--- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index dc9b0af45fe..cfffd1a1d6a 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -4250,7 +4250,7 @@ internal static IEnumerable CompleteFilename(CompletionContext } // Sorting the results by the path - var sortedPsobjs = psobjs.OrderBy(a => a, new ItemPathComparer()); + var sortedPsobjs = psobjs.OrderBy(a => a, new ItemPathComparer(wordToComplete)); foreach (PSObject psobj in sortedPsobjs) { @@ -6672,6 +6672,13 @@ internal static bool IsAmpersandNeeded(CompletionContext context, bool defaultCh private class ItemPathComparer : IComparer { + private String _baseWord; + + public ItemPathComparer(String baseWord) + { + _baseWord = baseWord + ".*"; + } + public int Compare(PSObject x, PSObject y) { var xPathInfo = PSObject.Base(x) as PathInfo; @@ -6701,7 +6708,21 @@ public int Compare(PSObject x, PSObject y) if (string.IsNullOrEmpty(xPath) || string.IsNullOrEmpty(yPath)) Diagnostics.Assert(false, "Base object of item PSObject should be either PathInfo or FileSystemInfo"); - return String.Compare(xPath, yPath, StringComparison.CurrentCultureIgnoreCase); + var result = String.Compare(xPath, yPath, StringComparison.CurrentCultureIgnoreCase); +#if UNIX + if (result == 0) + { + if (Regex.IsMatch(xPath, _baseWord)) + { + return -1; + } + else + { + return 1; + } + } +#endif + return result; } } diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index 192b32685c1..f9ec57bb656 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -198,8 +198,8 @@ Describe "TabCompletion" -Tags CI { Context "File name completion" { BeforeAll { $oneSubDirLowerTest = "onesubd" - $oneSubDirUpperTest = "oNeSubD" - $oneSubDirTest = "oneSubDir" + $oneSubDirUpperTest = "oneSubD" + $oneSubDirTest = "onesubdir" $tempDir = Join-Path -Path $TestDrive -ChildPath "baseDir" $oneSubDir = Join-Path -Path $tempDir -ChildPath $oneSubDirTest @@ -211,6 +211,13 @@ Describe "TabCompletion" -Tags CI { New-Item -Path $oneSubDirPrime -ItemType Directory -Force > $null New-Item -Path $twoSubDir -ItemType Directory -Force > $null + if (!$IsWindows) { + $oneSubDirTest2 = "oneSubDir" + $oneSubDir2 = Join-Path -Path $tempDir -ChildPath $oneSubDirTest2 + New-Item -Path $oneSubDir2 -ItemType Directory -Force > $null + } + + $testCases = @( @{ inputStr = "ab"; name = "abc"; localExpected = ".${separator}abc"; oneSubExpected = "..${separator}abc"; twoSubExpected = "..${separator}..${separator}abc" } @{ inputStr = "asaasas"; name = "asaasas!popee"; localExpected = ".${separator}asaasas!popee"; oneSubExpected = "..${separator}asaasas!popee"; twoSubExpected = "..${separator}..${separator}asaasas!popee" } @@ -245,7 +252,7 @@ Describe "TabCompletion" -Tags CI { Pop-Location } - It "TabCompletion should be case-insensitive for file names" { + It "TabCompletion should be case-insensitive for file names on Windows" -Skip:(!$IsWindows) { Push-Location -Path $tempDir $res = TabExpansion2 -inputScript $oneSubDirLowerTest -cursorColumn $oneSubDirLowerTest.Length $res.CompletionMatches.Count | Should BeGreaterThan 0 @@ -256,6 +263,17 @@ Describe "TabCompletion" -Tags CI { $res.CompletionMatches[0].CompletionText | Should Be ".${separator}$oneSubDirTest" } + It "TabCompletion should be case-sensitive for file names on Unix" -Skip:($IsWindows) { + Push-Location -Path $tempDir + $res = TabExpansion2 -inputScript $oneSubDirLowerTest -cursorColumn $oneSubDirLowerTest.Length + $res.CompletionMatches.Count | Should BeGreaterThan 0 + $res.CompletionMatches[0].CompletionText | Should BeExactly ".${separator}$oneSubDirTest" + + $res = TabExpansion2 -inputScript $oneSubDirUpperTest -cursorColumn $oneSubDirUpperTest.Length + $res.CompletionMatches.Count | Should BeGreaterThan 0 + $res.CompletionMatches[0].CompletionText | Should BeExactly ".${separator}$oneSubDirTest2" + } + It "Input '' should successfully complete" -TestCases $testCases { param ($inputStr, $localExpected)