From 4accb73eb50858a2f73458452315b9e09771ab24 Mon Sep 17 00:00:00 2001 From: Nick Date: Sun, 18 Apr 2021 11:11:02 +1000 Subject: [PATCH 01/16] Add autojump functionality. --- cd-extras/cd-extras.psm1 | 12 +- cd-extras/private/CompleteAncestors.ps1 | 9 +- cd-extras/private/CompleteFrecent.ps1 | 11 ++ cd-extras/private/CompletePaths.ps1 | 2 +- cd-extras/private/CompleteRecent.ps1 | 11 ++ cd-extras/private/CompleteStack.ps1 | 9 +- cd-extras/private/Core.ps1 | 129 ++++++++++++++++++--- cd-extras/public/Aliases.ps1 | 3 +- cd-extras/public/Get-FrecentLocation.ps1 | 13 +++ cd-extras/public/Get-RecentLocation.ps1 | 13 +++ cd-extras/public/Remove-RecentLocation.ps1 | 24 ++++ cd-extras/public/Set-CdExtrasOption.ps1 | 26 ++++- cd-extras/public/Set-FrecentLocation.ps1 | 47 ++++++++ cd-extras/public/Set-LocationEx.ps1 | 4 +- cd-extras/public/Set-RecentLocation.ps1 | 47 ++++++++ cd-extras/public/Step-Between.ps1 | 25 ---- cd-extras/public/_Classes.ps1 | 22 +++- readme.md | 21 ++-- 18 files changed, 352 insertions(+), 76 deletions(-) create mode 100644 cd-extras/private/CompleteFrecent.ps1 create mode 100644 cd-extras/private/CompleteRecent.ps1 create mode 100644 cd-extras/public/Get-FrecentLocation.ps1 create mode 100644 cd-extras/public/Get-RecentLocation.ps1 create mode 100644 cd-extras/public/Remove-RecentLocation.ps1 create mode 100644 cd-extras/public/Set-FrecentLocation.ps1 create mode 100644 cd-extras/public/Set-RecentLocation.ps1 delete mode 100644 cd-extras/public/Step-Between.ps1 diff --git a/cd-extras/cd-extras.psm1 b/cd-extras/cd-extras.psm1 index f5cf4ce..b59682d 100644 --- a/cd-extras/cd-extras.psm1 +++ b/cd-extras/cd-extras.psm1 @@ -4,19 +4,25 @@ Get-ChildItem $PSScriptRoot/private/*.ps1 | % { . $_.FullName } Get-ChildItem $PSScriptRoot/public/*.ps1 | % { . $_.FullName } # remove stupid phantom module -Get-Module | ? Path -eq ("$PSScriptRoot/public/_Classes.ps1" | Resolve-Path) | Remove-Module +Get-Module | Where Path -eq ("$PSScriptRoot/public/_Classes.ps1" | Resolve-Path) | Remove-Module $global:cde = if ((Test-Path variable:cde) -and $cde -is [System.Collections.IDictionary]) { - New-Object -Type CdeOptions -Property $global:cde + [CdeOptions]$cde } else { - New-Object -Type CdeOptions + [CdeOptions]::new() } +RegisterCompletions @('Step-Up') 'n' { CompleteAncestors @args } +RegisterCompletions @('Undo-Location', 'Redo-Location') 'n' { CompleteStack @args } +RegisterCompletions @('Set-RecentLocation') 'NamePart' { CompleteRecent @args } +RegisterCompletions @('Set-FrecentLocation') 'NamePart' { CompleteFrecent @args } + # some set up happens in Set-Option so make sure to call it here Set-CdExtrasOption -Option 'AUTO_CD' -Value $global:cde.AUTO_CD $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { + $Script:bg.Dispose() $ExecutionContext.SessionState.InvokeCommand.CommandNotFoundAction = $null Set-Item Alias:cd $cdAlias Remove-Variable cde -Scope Global -ErrorAction Ignore diff --git a/cd-extras/private/CompleteAncestors.ps1 b/cd-extras/private/CompleteAncestors.ps1 index 01b7015..653c027 100644 --- a/cd-extras/private/CompleteAncestors.ps1 +++ b/cd-extras/private/CompleteAncestors.ps1 @@ -1,15 +1,14 @@ function CompleteAncestors { param($commandName, $parameterName, $wordToComplete) $ups = Get-Ancestors + if (!$ups) { return } $valueToMatch = $wordToComplete | RemoveSurroundingQuotes $normalised = $valueToMatch | NormaliseAndEscape - if (!$ups) { return } - - $ups | where Path -eq $valueToMatch | - DefaultIfEmpty { $ups | where Name -match $normalised } | - DefaultIfEmpty { $ups | where Path -match $normalised } | + $ups | Where Path -eq $valueToMatch | + DefaultIfEmpty { $ups | Where Name -match $normalised } | + DefaultIfEmpty { $ups | Where Path -match $normalised } | IndexedComplete | DefaultIfEmpty { $null } } diff --git a/cd-extras/private/CompleteFrecent.ps1 b/cd-extras/private/CompleteFrecent.ps1 new file mode 100644 index 0000000..5a0f811 --- /dev/null +++ b/cd-extras/private/CompleteFrecent.ps1 @@ -0,0 +1,11 @@ +function CompleteFrecent { + param($commandName, $parameterName, $wordToComplete) + + $recents = Get-FrecentLocation $wordToComplete + + if (!$recents) { return } + + @($recents) | Where Path -match ($wordToComplete | RemoveSurroundingQuotes | RemoveTrailingSeparator | Escape) | + IndexedComplete $false | + DefaultIfEmpty { $null } +} diff --git a/cd-extras/private/CompletePaths.ps1 b/cd-extras/private/CompletePaths.ps1 index a7d3c8d..20cba4a 100644 --- a/cd-extras/private/CompletePaths.ps1 +++ b/cd-extras/private/CompletePaths.ps1 @@ -110,7 +110,7 @@ function CompletePaths { $cde.CDABLE_VARS -and $completions.Length -lt $maxCompletions -and $wordToComplete -match '[^/\\]+' -and # separate variable from slashes before or after it - ($maybeVar = Get-Variable "$($Matches[0])*" -ValueOnly | where { Test-Path $_ -PathType Container }) + ($maybeVar = Get-Variable "$($Matches[0])*" -ValueOnly | Where { Test-Path $_ -PathType Container }) ) { Expand-Path @switches ($wordToExpand -replace $Matches[0], $maybeVar) } diff --git a/cd-extras/private/CompleteRecent.ps1 b/cd-extras/private/CompleteRecent.ps1 new file mode 100644 index 0000000..57c307d --- /dev/null +++ b/cd-extras/private/CompleteRecent.ps1 @@ -0,0 +1,11 @@ +function CompleteRecent { + param($commandName, $parameterName, $wordToComplete) + + $recents = Get-RecentLocation $wordToComplete + + if (!$recents) { return } + + @($recents) | Where Path -match ($wordToComplete | RemoveSurroundingQuotes | RemoveTrailingSeparator | Escape) | + IndexedComplete $false | + DefaultIfEmpty { $null } +} diff --git a/cd-extras/private/CompleteStack.ps1 b/cd-extras/private/CompleteStack.ps1 index 47f43bc..c9206dc 100644 --- a/cd-extras/private/CompleteStack.ps1 +++ b/cd-extras/private/CompleteStack.ps1 @@ -1,13 +1,9 @@ function CompleteStack { - param($commandName, $parameterName, $wordToComplete, $commandAst, $boundParameters) + param($commandName, $parameterName, $wordToComplete) $stack = if ( - $commandName -and $commandName -match 'Redo' -or - ( - $aliased = (Get-Alias $commandName -ea Ignore).ResolvedCommandName -and - $aliased -match 'Redo' - ) + ($aliased = (Get-Alias $commandName -ea Ignore).ResolvedCommandName -and $aliased -match 'Redo') ) { (Get-Stack -Redo) } else { (Get-Stack -Undo) } @@ -15,5 +11,6 @@ function CompleteStack { @($stack) | Where Path -match ($wordToComplete | RemoveSurroundingQuotes | RemoveTrailingSeparator | Escape) | IndexedComplete | + Select -First $cde.MaxRecentCompletions | DefaultIfEmpty { $null } } diff --git a/cd-extras/private/Core.ps1 b/cd-extras/private/Core.ps1 index 8396d3d..7592e67 100644 --- a/cd-extras/private/Core.ps1 +++ b/cd-extras/private/Core.ps1 @@ -1,10 +1,12 @@ -${Script:/} = [System.IO.Path]::DirectorySeparatorChar -$Script:undoStack = [System.Collections.Stack]::new() -$Script:redoStack = [System.Collections.Stack]::new() -enum CycleDirection { Undo; Redo } -$Script:cycleDirection = [CycleDirection]::Undo # used by Step-Between +${Script:/} = [IO.Path]::DirectorySeparatorChar +$Script:undoStack = [Collections.Stack]::new() +$Script:redoStack = [Collections.Stack]::new() +# Dictionary[dirName, (accessTime, accessCount)] +$Script:recent = [Collections.Generic.Dictionary[string, RecentDir]]::new() +$Script:recentHash = $null $Script:logger = { Write-Verbose ($args[0] | ConvertTo-Json) } +$Script:bg function DefaultIfEmpty([scriptblock] $default) { Begin { $any = $false } @@ -86,9 +88,9 @@ function GetStackIndex([array]$stack, [string]$namepart) { ( $items = $stack -eq ($namepart | Normalise | RemoveTrailingSeparator) # full path match ) -or ( - $items = $stack | ? { ($_ | Split-Path -Leaf) -eq $namepart } # full leaf match + $items = $stack.Where{ ($_ | Split-Path -Leaf) -eq $namepart } # full leaf match ) -or ( - $items = $stack | ? { ($_ | Split-Path -Leaf) -Match "^$($namepart | NormaliseAndEscape)" } # leaf starts with + $items = $stack.Where{ ($_ | Split-Path -Leaf) -Match "^$($namepart | NormaliseAndEscape)" } # leaf starts with ) -or ( $items = $stack -match ($namepart | NormaliseAndEscape) # anything... ) | Out-Null @@ -96,14 +98,14 @@ function GetStackIndex([array]$stack, [string]$namepart) { [array]::indexOf($stack, ($items | select -First 1)) } -function IndexedComplete() { +function IndexedComplete([bool] $IndexedCompletion = $cde.IndexedCompletion) { Begin { $items = @() } Process { $items += $_ } End { $items | % { $completionText = - if ($cde.IndexedCompletion -and @($items).Count -gt 1) { "$($_.n)" } + if ($IndexedCompletion -and @($items).Count -gt 1) { "$($_.n)" } else { $_.path | SurroundAndTerminate } $listItemText = "$($_.n). $($_.name)" @@ -126,20 +128,115 @@ function IndexPaths( $rootLabel = 'root' # this on happens on *nix ) { $xs = $xs -ne '' - if (!$xs.Length) { return } + if (!$xs) { return } - 1..$xs.Length | % { + $i = 0 + $xs.ForEach{ [IndexedPath] @{ - n = $_ - Name = $xs[$_ - 1] | Split-Path -Leaf | DefaultIfEmpty { $rootLabel } - Path = $xs[$_ - 1] - } - } + n = ++$i + Name = $_ | Split-Path -Leaf | DefaultIfEmpty { $rootLabel } + Path = $_ + } } } function RegisterCompletions([string[]] $commands, $param, $target) { Register-ArgumentCompleter -CommandName $commands -ParameterName $param -ScriptBlock $target } +function RefreshRecent() { + # assumes we already know the file exists + if (!$cde.RECENT_DIRS_FILE) { return } + + $currentHash = (Get-FileHash -LiteralPath $cde.RECENT_DIRS_FILE).Hash.ToString() + if ($currentHash -ne $recentHash) { + WriteLog 'RecentDirs file has changed' + $recent.Clear() + (Import-Csv $cde.RECENT_DIRS_FILE).ForEach{ $recent[$_.Path] = [RecentDir]$_ } + } +} + +function RecentsByTermWithSort([string[]] $terms, [scriptblock] $sort, [int] $first) { + function MatchesTerms([string] $path) { + function MatchPath() { + $indexes = $terms.ForEach{ $path.IndexOf($_, [System.StringComparison]::CurrentCultureIgnoreCase) }.Where{ $_ -gt 0 } + $indexes.Count -eq $terms.Count -and (!(Compare-Object -SyncWindow 0 $indexes ($indexes | sort))) + } + function MatchLeaf() { (Split-Path -Leaf $path) -match $terms[-1] } + + if (!$terms) { return $true } + (MatchPath) -and (MatchLeaf) + } + + RefreshRecent + $recent.Values.Where( { ($_.Path -ne $pwd) -and (MatchesTerms $_.Path) }, 'First', $first) | + sort $sort -Descending | + select -Expand Path +} + +function GetFrecent([int] $first = $cde.MaxRecentCompletions, [string[]] $terms) { + function FrecencyFactor([ulong] $lastEntered) { + $now = [System.DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() + + if ($lastEntered -gt ($now - 1000 * 60 * 60)) { 4 } # past hour + elseif ($lastEntered -gt ($now - 1000 * 60 * 60 * 24)) { 2 } # past day + elseif ($lastEntered -gt ($now - 1000 * 60 * 60 * 24 * 7)) { 1 / 2 } # past week + else { 1 / 4 } + } + + RecentsByTermWithSort $terms { $_.EnterCount * (FrecencyFactor $_.LastEntered) } $first +} + +function GetRecent([int] $first, [string[]] $terms) { + RecentsByTermWithSort $terms { $_.LastEntered } $first +} + +function SaveRecent($path) { + if ($path -in ($cde.RECENT_DIRS_EXCLUDE | Resolve-Path).Path) { return } + + $current = $recent[$path] + $accessCount = if ($current) { $current.EnterCount + 1 } else { 1 } + + $recent[$path] = [RecentDir] @{ + Path = $path + LastEntered = [System.DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() + EnterCount = $accessCount + } + + if ($recent.Count -gt $cde.MaxRecentDirs) { + $null = $recent.Remove((GetRecent $cde.MaxRecentDirs | select -last 1)) + } + + PersistRecent +} + +function RemoveRecent([string[]] $dirs) { + $dirs | % { $recent.remove($_) } | Out-Null + PersistRecent +} + +function PersistRecent() { + if ($cde.RECENT_DIRS_FILE) { + if (!$bg) { InitRunspace } + $bg.Stop() + $null = $bg.BeginInvoke() + } +} + +function InitRunspace() { + # infra for backgrounding recent dirs persistence + $script:bg = [PowerShell]::Create() + $null = $bg.AddScript( { + $recent.Values | Export-Csv -LiteralPath $cde.RECENT_DIRS_FILE + $Script:recentHash = (Get-FileHash $cde.RECENT_DIRS_FILE).Hash.ToString() + } ) + + $runspace = [RunspaceFactory]::CreateRunspace() + $runspace.Open() + $runspace.SessionStateProxy.SetVariable('recent', $recent) + $runspace.SessionStateProxy.SetVariable('recentHash', $recentHash) + $runspace.SessionStateProxy.SetVariable('cde', $cde) + $bg.Runspace = $runspace +} + function WriteLog($message) { &$logger $message } diff --git a/cd-extras/public/Aliases.ps1 b/cd-extras/public/Aliases.ps1 index 1f2e273..532185c 100644 --- a/cd-extras/public/Aliases.ps1 +++ b/cd-extras/public/Aliases.ps1 @@ -1,12 +1,13 @@ Set-Alias .. Step-Up Set-Alias up Step-Up -Set-Alias cdb Step-Between Set-Alias gup Get-Up Set-Alias xup Get-Ancestors Set-Alias xpa Expand-Path Set-Alias cd- Undo-Location Set-Alias cd+ Redo-Location Set-Alias cd: Switch-LocationPart +Set-Alias cdr Set-RecentLocation +Set-Alias cdf Set-FrecentLocation Set-Alias dirs Get-Stack Set-Alias dirsc Clear-Stack Set-Alias setocd Set-CdExtrasOption diff --git a/cd-extras/public/Get-FrecentLocation.ps1 b/cd-extras/public/Get-FrecentLocation.ps1 new file mode 100644 index 0000000..f229148 --- /dev/null +++ b/cd-extras/public/Get-FrecentLocation.ps1 @@ -0,0 +1,13 @@ +function Get-FrecentLocation { + + [OutputType([IndexedPath])] + [CmdletBinding(DefaultParameterSetName = '')] + param( + [Parameter(ParameterSetName = 'First')] [ushort] $First = $cde.MaxRecentCompletions, + [Parameter(ValueFromRemainingArguments)] [string[]] $Terms + ) + + $recents = @(GetFrecent $First $Terms) + + if ($recents.Count) { IndexPaths $recents } +} diff --git a/cd-extras/public/Get-RecentLocation.ps1 b/cd-extras/public/Get-RecentLocation.ps1 new file mode 100644 index 0000000..4e4cdff --- /dev/null +++ b/cd-extras/public/Get-RecentLocation.ps1 @@ -0,0 +1,13 @@ +function Get-RecentLocation { + + [OutputType([IndexedPath])] + [CmdletBinding(DefaultParameterSetName = '')] + param( + [Parameter(ParameterSetName = 'First')] [ushort] $First = $cde.MaxRecentCompletions, + [Parameter(ValueFromRemainingArguments)] [string[]] $Terms + ) + + $recents = @(GetRecent $First $Terms) + + if ($recents.Count) { IndexPaths $recents } +} diff --git a/cd-extras/public/Remove-RecentLocation.ps1 b/cd-extras/public/Remove-RecentLocation.ps1 new file mode 100644 index 0000000..6c724df --- /dev/null +++ b/cd-extras/public/Remove-RecentLocation.ps1 @@ -0,0 +1,24 @@ +function Remove-RecentLocation { + + [OutputType([void])] + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory, ValueFromPipeline)] + [string] $Pattern + ) + + Begin { + $recents = @(GetRecent $cde.MaxRecentDirs); $accepted = @() + } + + Process { + $accepted += $recents.Where{ + !($_ -in $accepted) -and + ($_ -like $Pattern -or (Split-Path -Leaf $_) -eq $Pattern) -and + ($PSCmdlet.ShouldProcess($_)) } + } + + End { + if ($accepted) { RemoveRecent $accepted } + } +} diff --git a/cd-extras/public/Set-CdExtrasOption.ps1 b/cd-extras/public/Set-CdExtrasOption.ps1 index 0b4b35e..a15e9f1 100644 --- a/cd-extras/public/Set-CdExtrasOption.ps1 +++ b/cd-extras/public/Set-CdExtrasOption.ps1 @@ -57,17 +57,34 @@ function Set-CdExtrasOption { if ($Option -in $completionTypes) { if ($Global:cde.$option -notcontains $value) { - $value | ? { $Global:cde.$option -notcontains $_ } | % { $Global:cde.$option += $_ } + $value | Where { $Global:cde.$option -notcontains $_ } | % { $Global:cde.$option += $_ } } } else { $Global:cde.$option = $value } - $isUnderTest = { $Script:__cdeUnderTest -and !($Script:__cdeUnderTest = $false) } + if ($cde.RECENT_DIRS_FILE) { + # save recent dirs from memory when dirs file first set + if ($recent.Count) { PersistRecent } + + # load recent dirs into memory at startup + elseif (Test-Path $cde.RECENT_DIRS_FILE) { + $dirs = Import-Csv $cde.RECENT_DIRS_FILE + $dirs.ForEach{ $recent[$_.Path] = [RecentDir]$_ } + } + + else { + Add-Content $cde.RECENT_DIRS_FILE '' + } + } - RegisterCompletions @('Step-Up') 'n' { CompleteAncestors @args } - RegisterCompletions @('Undo-Location', 'Redo-Location') 'n' { CompleteStack @args } + $cde.RECENT_DIRS_EXCLUDE = $cde.RECENT_DIRS_EXCLUDE.ForEach{ Resolve-Path $_ } + $cde.RECENT_DIRS_FILE = if ($cde.RECENT_DIRS_FILE) { + $path = $cde.RECENT_DIRS_FILE -replace '~', $HOME + $Script:recentHash = $path | Where { Test-Path $_ } | Get-FileHash | Select -Expand Hash + $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath( $path ) + } if ($cde.DirCompletions) { RegisterCompletions $cde.DirCompletions 'Path' { CompletePaths -dirsOnly @args } @@ -79,6 +96,7 @@ function Set-CdExtrasOption { RegisterCompletions $cde.PathCompletions 'Path' { CompletePaths @args } } + $isUnderTest = { $Script:__cdeUnderTest -and !($Script:__cdeUnderTest = $false) } if ($cde.AUTO_CD) { CommandNotFound @(AutoCd) $isUnderTest } diff --git a/cd-extras/public/Set-FrecentLocation.ps1 b/cd-extras/public/Set-FrecentLocation.ps1 new file mode 100644 index 0000000..26b8d89 --- /dev/null +++ b/cd-extras/public/Set-FrecentLocation.ps1 @@ -0,0 +1,47 @@ +function Set-FrecentLocation { + [OutputType([void])] + [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'n')] + param( + [Parameter(ParameterSetName = 'n', Position = 0)] + [ushort] $n = 1, + + [Parameter(ParameterSetName = 'named', Position = 0)] + [string[]] $NamePart, + + [Alias("l")] + [Parameter(ParameterSetName = 'list', Mandatory)] + [switch] $List, + [Parameter(ParameterSetName = 'list')] + [ushort] $First = $cde.MaxRecentCompletions, + [Parameter(ParameterSetName = 'list', ValueFromRemainingArguments)] + [string[]] $Terms, + + [Alias("p")] + [Parameter(ParameterSetName = 'prune', Mandatory)] + [switch] $Prune, + [Parameter(ParameterSetName = 'prune', Position = 1, Mandatory, ValueFromPipeline)] + [string] $PrunePattern, + + [switch] $PassThru + ) + + if ($PSCmdlet.ParameterSetName -eq 'n' -and $n -ge 1) { + $recents = @(GetFrecent $n $NamePart) + if ($recents.Count -ge $n) { Set-LocationEx $recents[$n - 1] -PassThru:$PassThru } + } + + if ($PSCmdlet.ParameterSetName -eq 'named') { + $recents = @(GetFrecent 1 $NamePart) + if ($recents) { Set-LocationEx $recents[0] -PassThru:$PassThru } + elseif ($cde.RecentDirsFallThrough -and $NamePart.Length -eq 1) { Set-LocationEx $NamePart[0] -PassThru:$PassThru } + else { Write-Error "Could not find '$NamePart' in recent locations." -ErrorAction Stop } + } + + if ($PSCmdlet.ParameterSetName -eq 'list' -and $List) { + Get-FrecentLocation -First $First -Terms $Terms + } + + if ($PSCmdlet.ParameterSetName -eq 'prune' -and $Prune) { + Remove-RecentLocation -Pattern $PrunePattern + } +} diff --git a/cd-extras/public/Set-LocationEx.ps1 b/cd-extras/public/Set-LocationEx.ps1 index e28b004..920ebd2 100644 --- a/cd-extras/public/Set-LocationEx.ps1 +++ b/cd-extras/public/Set-LocationEx.ps1 @@ -94,7 +94,7 @@ Function Set-LocationEx { [CmdletBinding( DefaultParameterSetName = 'Path', - SupportsTransactions = $true, + SupportsTransactions, HelpUri = 'https://go.microsoft.com/fwlink/?LinkID=113397') ] param( @@ -196,7 +196,7 @@ Function Set-LocationEx { if ($PWD.Path -ne $startLocation) { $redoStack.Clear() $undoStack.Push($startLocation) - $Script:cycleDirection = [CycleDirection]::Undo + SaveRecent $PWD.Path } if ($steppablePipeline) { diff --git a/cd-extras/public/Set-RecentLocation.ps1 b/cd-extras/public/Set-RecentLocation.ps1 new file mode 100644 index 0000000..b273313 --- /dev/null +++ b/cd-extras/public/Set-RecentLocation.ps1 @@ -0,0 +1,47 @@ +function Set-RecentLocation { + [OutputType([void])] + [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'n')] + param( + [Parameter(ParameterSetName = 'n', Position = 0)] + [ushort] $n = 1, + + [Parameter(ParameterSetName = 'named', Position = 0)] + [string[]] $NamePart, + + [Alias("l")] + [Parameter(ParameterSetName = 'list', Mandatory)] + [switch] $List, + [Parameter(ParameterSetName = 'list')] + [ushort] $First = $cde.MaxRecentCompletions, + [Parameter(ParameterSetName = 'list', ValueFromRemainingArguments)] + [string[]] $Terms, + + [Alias("p")] + [Parameter(ParameterSetName = 'prune', Mandatory)] + [switch] $Prune, + [Parameter(ParameterSetName = 'prune', Position = 1, Mandatory, ValueFromPipeline)] + [string] $PrunePattern, + + [switch] $PassThru + ) + + if ($PSCmdlet.ParameterSetName -eq 'n' -and $n -ge 1) { + $recents = @(GetRecent $n $NamePart) + if ($recents.Count -ge $n) { Set-LocationEx $recents[$n - 1] } + } + + if ($PSCmdlet.ParameterSetName -eq 'named') { + $recents = @(GetRecent 1 $NamePart) + if ($recents) { Set-LocationEx $recents[0] } + elseif ($cde.RecentDirsFallThrough -and $NamePart.Length -eq 1) { Set-LocationEx $NamePart[0] } + else { Write-Error "Could not find '$NamePart' in recent locations." -ErrorAction Stop } + } + + if ($PSCmdlet.ParameterSetName -eq 'list' -and $List) { + Get-RecentLocation -First $First -Terms $Terms + } + + if ($PSCmdlet.ParameterSetName -eq 'prune' -and $Prune) { + Remove-RecentLocation -Pattern $PrunePattern @args + } +} diff --git a/cd-extras/public/Step-Between.ps1 b/cd-extras/public/Step-Between.ps1 deleted file mode 100644 index e954ca5..0000000 --- a/cd-extras/public/Step-Between.ps1 +++ /dev/null @@ -1,25 +0,0 @@ -<# -.SYNOPSIS -Toggle between undo and redo of the last location on the stack. - -.EXAMPLE -# toggles between the two most recent directories -PS C:\Windows\> cd system32 -PS C:\Windows\System32> cdb -PS C:\Windows\> cdb -PS C:\Windows\System32> _ -#> - -function Step-Between { - [OutputType([void], [Management.Automation.PathInfo])] - param ([switch] $PassThru) - - if ($Script:cycleDirection -eq [CycleDirection]::Undo) { - Undo-Location -PassThru:$PassThru - $Script:cycleDirection = [CycleDirection]::Redo - } - else { - Redo-Location -PassThru:$PassThru - $Script:cycleDirection = [CycleDirection]::Undo - } -} diff --git a/cd-extras/public/_Classes.ps1 b/cd-extras/public/_Classes.ps1 index 35db0f9..d38d73e 100644 --- a/cd-extras/public/_Classes.ps1 +++ b/cd-extras/public/_Classes.ps1 @@ -1,5 +1,5 @@ class IndexedPath { - [byte] $n + [ushort] $n [string] $Name [string] $Path @@ -10,7 +10,14 @@ class CdeOptions { [String[]] $CD_PATH = @() [bool] $AUTO_CD = $true [bool] $CDABLE_VARS = $false - [string] $NOARG_CD = '~' + [String] $NOARG_CD = '~' + [String] $RECENT_DIRS_FILE = $null + [String[]] $RECENT_DIRS_EXCLUDE = @() + [bool] $RecentDirsFallThrough = $true + [ushort] $MaxRecentDirs = 400 + [ushort] $MaxCompletions = 0 + [ushort] $MaxRecentCompletions = 100 + [ushort] $MaxMenuLength = 36 [Char[]] $WordDelimiters = '.', '_', '-' [UInt16] $MaxCompletions = 0 [UInt16] $MaxMenuLength = 36 @@ -19,10 +26,19 @@ class CdeOptions { [String[]] $FileCompletions = @() [bool] $ColorCompletion = $false [bool] $IndexedCompletion = (Get-Module PSReadLine) -and ( - Get-PSReadLineKeyHandler -Bound | ? Function -eq MenuComplete + Get-PSReadLineKeyHandler -Bound | Where Function -eq MenuComplete ) [ScriptBlock] $ToolTip = { param ($item, $isTruncated) "{0} $(if ($isTruncated) {'{1}'})" -f $item, "$([char]27)[3m(+additional results not displayed)$([char]27)[0m" } } + +class RecentDir { + [string] $Path + [ulong] $LastEntered + [uint] $EnterCount + [bool] $Starred + + [string] ToString() { return "{0}, {1}" -f $this.LastEntered, $this.Count } +} diff --git a/readme.md b/readme.md index 4c4c3b3..beab5fa 100644 --- a/readme.md +++ b/readme.md @@ -51,7 +51,7 @@ cd-extras # Navigation helper commands -**Quickly navigate backwards, forwards, upwards or between two directories** +**Quickly navigate backwards, forwards, upwards or into recently used directories*
[Watch]

@@ -64,8 +64,8 @@ _cd-extras_ provides the following navigation helpers and corresponding aliases - `Undo-Location`, (`cd-` or `~`) - `Redo-Location`, (`cd+` or `~~`) +- `Set-RecentLocation`, (`cdr`) - `Step-Up`, (`up`or `..`) -- `Step-Between`, (`cdb`) ```powershell [C:/Windows/System32]> up # or .. @@ -79,7 +79,7 @@ That's `cd-` and `cd+`, without a space. `cd -` and `cd +` (with a space) also w get [auto-completions](#completions). Repeated uses of `cd-` keep moving backwards towards the beginning of the stack rather than -toggling between the two most recent directories as in vanilla bash. Use `Step-Between` (`cdb`) +toggling between the two most recent directories as in vanilla bash. Use `Set-RecentLocation` (`cdr`) if you want to toggle between directories. ```powershell @@ -89,15 +89,15 @@ if you want to toggle between directories. [C:/Windows]> cd- [C:/Windows/System32]> cd+ [C:/Windows]> cd+ -[C:/]> cdb -[C:/Windows]> cdb +[C:/]> cdr +[C:/Windows]> cdr [C:/]> █ ``` ## Parameters -`up`, `cd+` and `cd-` each take a single optional argument: either a number of steps, `n`... +`up`, `cd+`, `cd-` and `cdr` each take a single optional argument: either a number of steps, `n`... ```powershell [C:/Windows/System32]> .. 2 # or `up 2` @@ -117,8 +117,9 @@ within the full path of each candidate directory⁽²⁾. [C:/Windows/System32]> cd drivers [C:/Windows/System32/drivers]> cd- win # [ex. 1] by leaf name [C:/Windows/]> cd+ 32/dr # [ex. 2] by full name -[C:/Windows/System32/drivers]> up win # by leaf name again -[C:/Windows]> █ +[C:/Windows/System32/drivers]> up win # by leaf name +[C:/Windows]> cdr drivers # by leaf again +[C:/Windows/System32/drivers]> █ ``` @@ -297,7 +298,7 @@ this... [~/projects/powershell/src]> cd -unix Set-LocationEx: A parameter cannot be found that matches parameter name 'unix'. -[~/projects/powershell/src]> cd `-unix # backtick escapes the hypen +[~/projects/powershell/src]> cd `-unix # backtick escapes the hyphen [~/projects/PowerShell/src/powershell-unix]> █ ``` @@ -885,7 +886,7 @@ function invokePrompt() { [Microsoft.PowerShell.PSConsoleReadLine]::InvokePrompt 'Alt+^' = { if (up -PassThru) { invokePrompt } } 'Alt+[' = { if (cd- -PassThru) { invokePrompt } } 'Alt+]' = { if (cd+ -PassThru) { invokePrompt } } - 'Alt+Backspace' = { if (cdb -PassThru) { invokePrompt } } + 'Alt+Backspace' = { if (cdr -PassThru) { invokePrompt } } }.GetEnumerator() | % { Set-PSReadLineKeyHandler $_.Name $_.Value } ``` From 5fff6776cb4e3665aa5f0db44e8ff3e9ffd9b031 Mon Sep 17 00:00:00 2001 From: Nick Date: Mon, 19 Apr 2021 19:09:00 +1000 Subject: [PATCH 02/16] Implement autojump related functionality. --- PSScriptAnalyzerSettings.psd1 | 5 +- cd-extras/cd-extras.psm1 | 11 +- cd-extras/private/CommandNotFound.ps1 | 7 +- cd-extras/private/CompleteAncestors.ps1 | 3 +- cd-extras/private/Core.ps1 | 88 ++- cd-extras/public/Add-Bookmark.ps1 | 35 + cd-extras/public/Aliases.ps1 | 2 + cd-extras/public/Expand-Path.ps1 | 19 +- cd-extras/public/Get-Bookmark.ps1 | 35 + cd-extras/public/Get-FrecentLocation.ps1 | 50 ++ cd-extras/public/Get-RecentLocation.ps1 | 46 ++ cd-extras/public/Remove-Bookmark.ps1 | 49 ++ cd-extras/public/Remove-RecentLocation.ps1 | 33 +- cd-extras/public/Set-CdExtrasOption.ps1 | 81 ++- cd-extras/public/Set-FrecentLocation.ps1 | 112 ++- cd-extras/public/Set-LocationEx.ps1 | 2 +- cd-extras/public/Set-RecentLocation.ps1 | 66 +- cd-extras/public/_Classes.ps1 | 42 +- coverage.xml | 1 + readme.md | 12 +- tests/cd-extras.Tests.ps1 | 286 +++++++- tests/sampleStructure.txt | 774 ++++++++++----------- 22 files changed, 1208 insertions(+), 551 deletions(-) create mode 100644 cd-extras/public/Add-Bookmark.ps1 create mode 100644 cd-extras/public/Get-Bookmark.ps1 create mode 100644 cd-extras/public/Remove-Bookmark.ps1 create mode 100644 coverage.xml diff --git a/PSScriptAnalyzerSettings.psd1 b/PSScriptAnalyzerSettings.psd1 index 43040fe..4271c65 100644 --- a/PSScriptAnalyzerSettings.psd1 +++ b/PSScriptAnalyzerSettings.psd1 @@ -1,3 +1,6 @@ @{ - ExcludeRules = @('PSAvoidUsingCmdletAliases') + ExcludeRules = @( + 'PSAvoidUsingCmdletAliases', + 'PSAvoidGlobalVars', + 'PSUseShouldProcessForStateChangingFunctions') } diff --git a/cd-extras/cd-extras.psm1 b/cd-extras/cd-extras.psm1 index b59682d..6d66ece 100644 --- a/cd-extras/cd-extras.psm1 +++ b/cd-extras/cd-extras.psm1 @@ -13,16 +13,15 @@ else { [CdeOptions]::new() } +(Get-Variable cde).Attributes.Add([ValidateScript]::new( { Set-CdExtrasOption -Validate } )) + RegisterCompletions @('Step-Up') 'n' { CompleteAncestors @args } RegisterCompletions @('Undo-Location', 'Redo-Location') 'n' { CompleteStack @args } -RegisterCompletions @('Set-RecentLocation') 'NamePart' { CompleteRecent @args } -RegisterCompletions @('Set-FrecentLocation') 'NamePart' { CompleteFrecent @args } - -# some set up happens in Set-Option so make sure to call it here -Set-CdExtrasOption -Option 'AUTO_CD' -Value $global:cde.AUTO_CD +RegisterCompletions @('Set-RecentLocation') 'Terms' { CompleteRecent @args } +RegisterCompletions @('Set-FrecentLocation') 'Terms' { CompleteFrecent @args } $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { - $Script:bg.Dispose() + if ($background) { $background.Dispose() } $ExecutionContext.SessionState.InvokeCommand.CommandNotFoundAction = $null Set-Item Alias:cd $cdAlias Remove-Variable cde -Scope Global -ErrorAction Ignore diff --git a/cd-extras/private/CommandNotFound.ps1 b/cd-extras/private/CommandNotFound.ps1 index ffda5f0..e935d20 100644 --- a/cd-extras/private/CommandNotFound.ps1 +++ b/cd-extras/private/CommandNotFound.ps1 @@ -9,11 +9,8 @@ function CommandNotFound($actions, $isUnderTest) { if ($CommandLookupEventArgs.CommandOrigin -ne 'Runspace' -and !(&$isUnderTest)) { return } $invocation = if ($isUnderTest) { $CommandName } else { $MyInvocation.Line } - # don't run as part of pipeline - if ($invocation -match "$([regex]::Escape($CommandName))\s*\|") { return } - - # don't run if no word characters given - if ($invocation -notmatch '\w|^\.{3,}$') { return } + # only match stuff that looks AUTO_CDish + if ($invocation -notmatch '^[\w~/\\\.]*$|^\.{3,}$') { return } $actions | % { &$_ $CommandName $CommandLookupEventArgs } }.GetNewClosure() diff --git a/cd-extras/private/CompleteAncestors.ps1 b/cd-extras/private/CompleteAncestors.ps1 index 653c027..94f531d 100644 --- a/cd-extras/private/CompleteAncestors.ps1 +++ b/cd-extras/private/CompleteAncestors.ps1 @@ -9,6 +9,5 @@ function CompleteAncestors { $ups | Where Path -eq $valueToMatch | DefaultIfEmpty { $ups | Where Name -match $normalised } | DefaultIfEmpty { $ups | Where Path -match $normalised } | - IndexedComplete | - DefaultIfEmpty { $null } + IndexedComplete } diff --git a/cd-extras/private/Core.ps1 b/cd-extras/private/Core.ps1 index 7592e67..794f53b 100644 --- a/cd-extras/private/Core.ps1 +++ b/cd-extras/private/Core.ps1 @@ -2,11 +2,9 @@ ${Script:/} = [IO.Path]::DirectorySeparatorChar $Script:undoStack = [Collections.Stack]::new() $Script:redoStack = [Collections.Stack]::new() -# Dictionary[dirName, (accessTime, accessCount)] $Script:recent = [Collections.Generic.Dictionary[string, RecentDir]]::new() -$Script:recentHash = $null $Script:logger = { Write-Verbose ($args[0] | ConvertTo-Json) } -$Script:bg +$Script:background function DefaultIfEmpty([scriptblock] $default) { Begin { $any = $false } @@ -142,19 +140,31 @@ function RegisterCompletions([string[]] $commands, $param, $target) { Register-ArgumentCompleter -CommandName $commands -ParameterName $param -ScriptBlock $target } +function ImportRecent() { + $dirs = Import-Csv $cde.RECENT_DIRS_FILE + $cde.recentHash = if ($h = Get-FileHash -LiteralPath $cde.RECENT_DIRS_FILE -ErrorAction Ignore) { + $h.Hash.ToString() + } + $dirs.ForEach{ + $dir = [RecentDir]$_ + $dir.Favour = $_.Favour -and [bool]::Parse($_.Favour) + $recent[$_.Path] = $dir + } +} + function RefreshRecent() { # assumes we already know the file exists if (!$cde.RECENT_DIRS_FILE) { return } $currentHash = (Get-FileHash -LiteralPath $cde.RECENT_DIRS_FILE).Hash.ToString() - if ($currentHash -ne $recentHash) { + if ($currentHash -ne $cde.recentHash) { WriteLog 'RecentDirs file has changed' $recent.Clear() - (Import-Csv $cde.RECENT_DIRS_FILE).ForEach{ $recent[$_.Path] = [RecentDir]$_ } + ImportRecent } } -function RecentsByTermWithSort([string[]] $terms, [scriptblock] $sort, [int] $first) { +function RecentsByTermWithSort([int] $first, [string[]] $terms, [scriptblock] $sort) { function MatchesTerms([string] $path) { function MatchPath() { $indexes = $terms.ForEach{ $path.IndexOf($_, [System.StringComparison]::CurrentCultureIgnoreCase) }.Where{ $_ -gt 0 } @@ -167,12 +177,12 @@ function RecentsByTermWithSort([string[]] $terms, [scriptblock] $sort, [int] $fi } RefreshRecent - $recent.Values.Where( { ($_.Path -ne $pwd) -and (MatchesTerms $_.Path) }, 'First', $first) | + $recent.Values.Where( { ($_.Path -ne $pwd) -and (MatchesTerms $_.Path) }) | sort $sort -Descending | - select -Expand Path + select -First $first -Expand Path } -function GetFrecent([int] $first = $cde.MaxRecentCompletions, [string[]] $terms) { +function GetFrecent([int] $first, [string[]] $terms) { function FrecencyFactor([ulong] $lastEntered) { $now = [System.DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() @@ -182,32 +192,54 @@ function GetFrecent([int] $first = $cde.MaxRecentCompletions, [string[]] $terms) else { 1 / 4 } } - RecentsByTermWithSort $terms { $_.EnterCount * (FrecencyFactor $_.LastEntered) } $first + function FavourFactor([bool] $isFavoured) { + ([int]$isFavoured * 1000) + 1 + } + + RecentsByTermWithSort $first $terms { + $_.EnterCount * (FrecencyFactor $_.LastEntered) * (FavourFactor $_.Favour) + } } function GetRecent([int] $first, [string[]] $terms) { - RecentsByTermWithSort $terms { $_.LastEntered } $first + RecentsByTermWithSort $first $terms { $_.LastEntered } } -function SaveRecent($path) { +function UpdateRecent($path, $favour = $false) { if ($path -in ($cde.RECENT_DIRS_EXCLUDE | Resolve-Path).Path) { return } - $current = $recent[$path] - $accessCount = if ($current) { $current.EnterCount + 1 } else { 1 } + $entry = + if (($current = $recent[$path])) { $current } + else { [RecentDir] @{ Path = $path; EnterCount = $favour } } - $recent[$path] = [RecentDir] @{ - Path = $path - LastEntered = [System.DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() - EnterCount = $accessCount + if (!$favour) { + $entry.LastEntered = [System.DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() + $entry.EnterCount++ + } + else { + $entry.Favour = $true } + $recent[$path] = $entry + if ($recent.Count -gt $cde.MaxRecentDirs) { - $null = $recent.Remove((GetRecent $cde.MaxRecentDirs | select -last 1)) + RemoveRecent ( + $recent.Values | + sort LastEntered | + select -First ($recent.Count - $cde.MaxRecentDirs) -expand Path ) } PersistRecent } +function Unfavour([RecentDir] $dir) { + if (!$dir.LastEntered) { RemoveRecent(@($dir.Path)) } + else { + $dir.Favour = $false + PersistRecent + } +} + function RemoveRecent([string[]] $dirs) { $dirs | % { $recent.remove($_) } | Out-Null PersistRecent @@ -215,28 +247,28 @@ function RemoveRecent([string[]] $dirs) { function PersistRecent() { if ($cde.RECENT_DIRS_FILE) { - if (!$bg) { InitRunspace } - $bg.Stop() - $null = $bg.BeginInvoke() + if (!$background) { InitRunspace } + $background.Stop() + $null = $background.BeginInvoke() } } function InitRunspace() { # infra for backgrounding recent dirs persistence - $script:bg = [PowerShell]::Create() - $null = $bg.AddScript( { + $script:background = [PowerShell]::Create() + $null = $background.AddScript( { $recent.Values | Export-Csv -LiteralPath $cde.RECENT_DIRS_FILE - $Script:recentHash = (Get-FileHash $cde.RECENT_DIRS_FILE).Hash.ToString() + $cde.recentHash = (Get-FileHash $cde.RECENT_DIRS_FILE).Hash.ToString() } ) $runspace = [RunspaceFactory]::CreateRunspace() $runspace.Open() $runspace.SessionStateProxy.SetVariable('recent', $recent) - $runspace.SessionStateProxy.SetVariable('recentHash', $recentHash) $runspace.SessionStateProxy.SetVariable('cde', $cde) - $bg.Runspace = $runspace + $background.Runspace = $runspace } function WriteLog($message) { - &$logger $message + $m = if ($message) { $message } else { '[null]' } + &$logger $m } diff --git a/cd-extras/public/Add-Bookmark.ps1 b/cd-extras/public/Add-Bookmark.ps1 new file mode 100644 index 0000000..b7917be --- /dev/null +++ b/cd-extras/public/Add-Bookmark.ps1 @@ -0,0 +1,35 @@ +<# +.SYNOPSIS +Bookmarks a directory to promote it to the top of the frecent locations list. + +.PARAMETER Path +The path to bookmark ($PWD by default). + +.EXAMPLE +PS C:\temp> # bookmark the current directory +PS C:\temp> mark +PS C:\temp> Get-Bookmark +C:\temp + +.EXAMPLE +PS C:\temp> # bookmark another directory +PS C:\temp> mark / +PS C:\temp> Get-Bookmark +C:\ + +.LINK +Get-Bookmark +Remove-Bookmark +Get-FrecentLocation +Set-FrecentLocation +#> + +function Add-Bookmark() { + + [OutputType([void])] + param( + [Parameter(Position = 0, ValueFromPipeline)] [string] $Path = $PWD + ) + + Process { UpdateRecent (Resolve-Path $Path) $true } +} diff --git a/cd-extras/public/Aliases.ps1 b/cd-extras/public/Aliases.ps1 index 532185c..17606f9 100644 --- a/cd-extras/public/Aliases.ps1 +++ b/cd-extras/public/Aliases.ps1 @@ -11,6 +11,8 @@ Set-Alias cdf Set-FrecentLocation Set-Alias dirs Get-Stack Set-Alias dirsc Clear-Stack Set-Alias setocd Set-CdExtrasOption +Set-Alias mark Add-Bookmark +Set-Alias unmark Remove-Bookmark Set-Alias '~' Undo-Location Set-Alias '~~' Redo-Location diff --git a/cd-extras/public/Expand-Path.ps1 b/cd-extras/public/Expand-Path.ps1 index 706a236..931075c 100644 --- a/cd-extras/public/Expand-Path.ps1 +++ b/cd-extras/public/Expand-Path.ps1 @@ -54,15 +54,16 @@ function Expand-Path { [OutputType([object])] [CmdletBinding()] param ( - [alias("Candidate")] - [parameter(ValueFromPipeline, Mandatory)] - [String] $Path, - [UInt16] $MaxResults = [UInt16]::MaxValue, - [String[]] $SearchPaths = $cde.CD_PATH, - [Char[]] $WordDelimiters = $cde.WordDelimiters, - [Switch] $File, - [Switch] $Directory, - [Switch] $Force + [Alias("Candidate")] + [Parameter(ValueFromPipeline, Mandatory)] + [SupportsWildcards()] + [string] $Path, + [ushort] $MaxResults = [ushort]::MaxValue, + [string[]] $SearchPaths = $cde.CD_PATH, + [char[]] $WordDelimiters = $cde.WordDelimiters, + [switch] $File, + [switch] $Directory, + [switch] $Force ) Process { diff --git a/cd-extras/public/Get-Bookmark.ps1 b/cd-extras/public/Get-Bookmark.ps1 new file mode 100644 index 0000000..6594112 --- /dev/null +++ b/cd-extras/public/Get-Bookmark.ps1 @@ -0,0 +1,35 @@ +<# +.SYNOPSIS +Retrieves the list of bookmarked locations, ordered by how often they've been used. + +.PARAMETER First +The number of bookmarks to return. (The entire list is returned by default.) + +.EXAMPLE +PS C:\temp> # get the entire list +PS C:\temp> Get-Bookmark +C:\someDir +C:\someOtherDir + +.EXAMPLE +PS C:\temp> # get the most used bookmark +PS C:\temp> Get-Bookmark 1 +C:\someDir + +.LINK +Add-Bookmark +Remove-Bookmark +Get-FrecentLocation +Set-FrecentLocation +#> + +function Get-Bookmark { + + [OutputType([string[]])] + param( + [Parameter(Position = 0)] [ushort] $First = $cde.MaxRecentCompletions + ) + $recent.Values.Where{ $_.Favour } | + sort EnterCount, LastEntered -Descending | + select -First $First -Expand Path +} diff --git a/cd-extras/public/Get-FrecentLocation.ps1 b/cd-extras/public/Get-FrecentLocation.ps1 index f229148..3030728 100644 --- a/cd-extras/public/Get-FrecentLocation.ps1 +++ b/cd-extras/public/Get-FrecentLocation.ps1 @@ -1,3 +1,53 @@ +<# +.SYNOPSIS +Retrieves a list of your most frecently used locations. (Excluding the current directory.) + +.PARAMETER First +The number of locations to return ($cde.MaxRecentCompletions by default). + +.PARAMETER Terms +Terms to match, separated with spaces or commas. The last term must match the leaf name +of a directory in order to be considered a match. The current directory is always excluded from +the list. + +.EXAMPLE +PS C:\temp> # get the entire list +PS C:\temp> Get-FrecentLocation + +n Name Path + - ---- ---- + 1 PowerShell C:\Temp\PowerShell + 2 thread C:\Temp\thread + 3 two C:\Temp\two + 4 abc_app C:\Temp\abc_app + 5 test C:\Temp\test + ... + +.EXAMPLE +PS C:\temp> # get locations matching the given terms +PS C:\temp> Get-FrecentLocation temp abc + +n Name Path +- ---- ---- +1 abc def C:\Temp\abc def +2 abc_app C:\Temp\abc_app +3 abc-infra C:\Temp\abc-infra + +.EXAMPLE +PS C:\temp> # get the first (most frecent) location matching the given terms +PS C:\temp> Get-FrecentLocation temp abc -f 1 + +n Name Path +- ---- ---- +1 abc def C:\Temp\abc def + +.LINK +Add-Bookmark +Remove-Bookmark +Set-FrecentLocation +Remove-RecentLocation +#> + function Get-FrecentLocation { [OutputType([IndexedPath])] diff --git a/cd-extras/public/Get-RecentLocation.ps1 b/cd-extras/public/Get-RecentLocation.ps1 index 4e4cdff..a5d17fe 100644 --- a/cd-extras/public/Get-RecentLocation.ps1 +++ b/cd-extras/public/Get-RecentLocation.ps1 @@ -1,3 +1,49 @@ +<# +.SYNOPSIS +Retrieves a list of your most recently used locations. + +.PARAMETER First +The number of locations to return ($cde.MaxRecentCompletions by default). + +.PARAMETER Terms +Terms to match, separated with spaces or commas. The last term must match the leaf name of a directory +in order to be considered a match. + +.EXAMPLE +PS C:\temp> # get the entire list +PS C:\temp> Get-RecentLocation + + n Name Path + - ---- ---- + 1 cd-extras C:\Users\Nick\projects\cd-extras + 2 C:\ C:\ + 3 thread C:\Temp\thread + 4 PowerShell C:\Temp\PowerShell + 5 two C:\Temp\two + ... + +.EXAMPLE +PS C:\temp> # get locations matching the given terms +PS C:\temp> Get-RecentLocation temp abc + +n Name Path +- ---- ---- +1 abc def C:\Temp\abc def +2 abc_app C:\Temp\abc_app +3 abc-infra C:\Temp\abc-infra + +.EXAMPLE +PS C:\temp> # get the first (most recent) location matching the given terms +PS C:\temp> Get-FrecentLocation temp abc -f 1 + +n Name Path +- ---- ---- +1 abc def C:\Temp\abc def + +.LINK +Set-RecentLocation +Remove-RecentLocation +#> function Get-RecentLocation { [OutputType([IndexedPath])] diff --git a/cd-extras/public/Remove-Bookmark.ps1 b/cd-extras/public/Remove-Bookmark.ps1 new file mode 100644 index 0000000..d9927d4 --- /dev/null +++ b/cd-extras/public/Remove-Bookmark.ps1 @@ -0,0 +1,49 @@ +<# +.SYNOPSIS +Remove bookmarks from one or more directories. + +.PARAMETER Pattern +The pattern to match. This should either be a leaf name of directories you want to unmark +or a PowerShell wildcard pattern to be matched against the full directory path. ($PWD by default.) + +.EXAMPLE +PS C:\temp> # unmark the current directory +PS C:\temp> unmark + +.EXAMPLE +PS C:\temp> # remove all bookmarks +PS C:\temp> unmark * + +.LINK +Get-Bookmark +Add-Bookmark +Get-FrecentLocation +Set-FrecentLocation +#> + +function Remove-Bookmark() { + + [OutputType([void])] + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Position = 0, ValueFromPipeline)] + [SupportsWildcards()] + [string] $Pattern = $PWD + ) + + Begin { + $recents = $recent.Values.Where{ $_.Favour } + $accepted = @() + } + + Process { + $accepted += $recents.Where{ + !($_ -in $accepted) -and + ($_.Path -like $Pattern -or (Split-Path -Leaf $_.Path) -eq $Pattern) -and + ($PSCmdlet.ShouldProcess($_.Path)) } + } + + End { + $accepted | % { Unfavour $_ } + } +} diff --git a/cd-extras/public/Remove-RecentLocation.ps1 b/cd-extras/public/Remove-RecentLocation.ps1 index 6c724df..5e5e338 100644 --- a/cd-extras/public/Remove-RecentLocation.ps1 +++ b/cd-extras/public/Remove-RecentLocation.ps1 @@ -1,14 +1,41 @@ +<# +.SYNOPSIS +Remove one or more directories from the recent locations list. Note that the list is shared by both +the Get-RecentLocation and Get-FrecentLocation commands, so removing a location will make it unavailable +in both commands. + +.PARAMETER Pattern +The pattern to match. This should either be a leaf name of directories to remove or a PowerShell wildcard +pattern to be matched against the full directory path. ($PWD by default.) + +.EXAMPLE +PS C:\temp> # remove the current directory +PS C:\temp> Remove-RecentLocation + +.EXAMPLE +PS C:\temp> # remove all recent locations +PS C:\temp> Remove-RecentLocation * + +.LINK +Get-RecentLocation +Set-RecentLocation +Get-FrecentLocation +Set-FrecentLocation +#> + function Remove-RecentLocation { [OutputType([void])] [CmdletBinding(SupportsShouldProcess)] param( - [Parameter(Mandatory, ValueFromPipeline)] - [string] $Pattern + [Parameter(Position = 0, ValueFromPipeline)] + [SupportsWildcards()] + [string] $Pattern = $PWD ) Begin { - $recents = @(GetRecent $cde.MaxRecentDirs); $accepted = @() + $recents = @(GetRecent $cde.MaxRecentDirs) + @($PWD) + $accepted = @() } Process { diff --git a/cd-extras/public/Set-CdExtrasOption.ps1 b/cd-extras/public/Set-CdExtrasOption.ps1 index a15e9f1..6cd53be 100644 --- a/cd-extras/public/Set-CdExtrasOption.ps1 +++ b/cd-extras/public/Set-CdExtrasOption.ps1 @@ -9,82 +9,85 @@ The option to update. The new value. .EXAMPLE -C:\> setocd AUTO_CD +PS C:\> setocd AUTO_CD Enables AUTO_CD .EXAMPLE -C:\> setocd AUTO_CD $false +PS C:\> setocd AUTO_CD $false Disables AUTO_CD .EXAMPLE -C:\> Set-CdExtrasOption -Option CD_PATH -Value @('/temp') +PS C:\> Set-CdExtrasOption -Option CD_PATH -Value @('/temp') Set the directory search paths to the single directory, '/temp' #> function Set-CdExtrasOption { - [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] - [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")] - [OutputType([void])] - [CmdletBinding()] + [OutputType([void], [bool])] + [CmdletBinding(DefaultParameterSetName = 'Set')] param ( [ArgumentCompleter( { $global:cde | Get-Member -Type Property -Name "$($args[2])*" | % Name })] - [Parameter(Mandatory)] - $Option, + [Parameter(ParameterSetName = 'Set', Mandatory, Position = 0)] + [string] $Option, - [parameter(ValueFromPipeline)] - $Value - ) + [Parameter(ParameterSetName = 'Set', Position = 1, ValueFromPipeline)] + $Value, - $flags = @( - 'AUTO_CD', - 'CDABLE_VARS' - 'ColorCompletion' - 'IndexedCompletion' + [Parameter(ParameterSetName = 'Validate', Mandatory, ValueFromPipeline)] + [switch] $Validate ) - if ($null -eq $Value -and $Option -in $flags) { - $Value = $true - } + if ($PSCmdlet.ParameterSetName -eq 'Set') { - $completionTypes = @( - 'PathCompletions' - 'DirCompletions' - 'FileCompletions' - ) + $flags = @( + 'AUTO_CD' + 'CDABLE_VARS' + 'ColorCompletion' + 'IndexedCompletion' + 'RecentDirsFallThrough' + ) - if ($Option -in $completionTypes) { - if ($Global:cde.$option -notcontains $value) { - $value | Where { $Global:cde.$option -notcontains $_ } | % { $Global:cde.$option += $_ } + if ($null -eq $Value -and $Option -in $flags) { + $Value = $true + } + + $completionTypes = @( + 'PathCompletions' + 'DirCompletions' + 'FileCompletions' + ) + + if ($Option -in $completionTypes) { + if ($Global:cde.$option -notcontains $value) { + $value | Where { $Global:cde.$option -notcontains $_ } | % { $Global:cde.$option += $_ } + } + } + else { + $Global:cde.$option = $value } - } - else { - $Global:cde.$option = $value } if ($cde.RECENT_DIRS_FILE) { + $path = $cde.RECENT_DIRS_FILE -replace '~', $HOME + $cde.RECENT_DIRS_FILE = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath( $path ) + # save recent dirs from memory when dirs file first set if ($recent.Count) { PersistRecent } # load recent dirs into memory at startup elseif (Test-Path $cde.RECENT_DIRS_FILE) { - $dirs = Import-Csv $cde.RECENT_DIRS_FILE - $dirs.ForEach{ $recent[$_.Path] = [RecentDir]$_ } + ImportRecent } else { Add-Content $cde.RECENT_DIRS_FILE '' + $global:cde.recentHash = (Get-FileHash $cde.RECENT_DIRS_FILE).Hash.ToString() } } $cde.RECENT_DIRS_EXCLUDE = $cde.RECENT_DIRS_EXCLUDE.ForEach{ Resolve-Path $_ } - $cde.RECENT_DIRS_FILE = if ($cde.RECENT_DIRS_FILE) { - $path = $cde.RECENT_DIRS_FILE -replace '~', $HOME - $Script:recentHash = $path | Where { Test-Path $_ } | Get-FileHash | Select -Expand Hash - $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath( $path ) - } if ($cde.DirCompletions) { RegisterCompletions $cde.DirCompletions 'Path' { CompletePaths -dirsOnly @args } @@ -103,4 +106,6 @@ function Set-CdExtrasOption { else { CommandNotFound @() $isUnderTest } + + if ($Validate) { return $true } } diff --git a/cd-extras/public/Set-FrecentLocation.ps1 b/cd-extras/public/Set-FrecentLocation.ps1 index 26b8d89..82848ea 100644 --- a/cd-extras/public/Set-FrecentLocation.ps1 +++ b/cd-extras/public/Set-FrecentLocation.ps1 @@ -1,4 +1,71 @@ +<# +.SYNOPSIS +Navigate to a frecently used location or work with the frecent locations list. + +.PARAMETER n +Navigate to the nth most frecent location. + +.PARAMETER Terms +Navigate to the most frecent location matching the given terms. This can be a single term or a comma separated +list. The last (or only) term must match the leaf name of a directory in order to be considered a match. + +.PARAMETER List +List the matching frecent locations instead of changing directory. Equivalent to the Get-FrecentLocation command. +The current directory is always excluded from the list. + +.PARAMETER ListTerms +Terms to matching when listing frecent locations. This can be a single term or a comma or space separated list. +The last (or only) term must match the leaf name of a directory in order to be considered a match. + +.PARAMETER First +When listing frecent locations, limits the list to the first n matches. + +.PARAMETER Prune +Prune the recent locations list. Equivalent to the Remove-RecentLocation command. + +.PARAMETER PrunePattern +The pattern to match when pruning recent locations. This should either be a leaf name of directories to remove +or a PowerShell wildcard pattern to be matched against the full directory path. ($PWD by default.) + +.PARAMETER Mark +Bookmarks a directory to promote it to the top of the frecent locations list. Equivalent to the Add-Bookmark command. + +.PARAMETER MarkPath +Path of the directory to bookmark. ($PWD by default.) + +.PARAMETER Unmark +Remove bookmarks from one or more directories. Equivalent to the Remove-Bookmark command. + +.PARAMETER UnmarkPattern +The pattern to match. This should either be a leaf name of directories you want to unmark or a PowerShell wildcard +pattern to be matched against the full directory path. ($PWD by default.) + +.EXAMPLE +PS ~> # navigate to the most frecent location matching the given terms +PS ~> cdf temp,py +PS C:\temp\python> + +.EXAMPLE +PS ~> # list frecent locations matching the given terms +PS ~> cdf -l temp,py + +n Name Path +- ---- ---- +1 python C:\Temp\python + +.EXAMPLE +PS ~> # remove the current directory from the recent locations list. +PS ~> cdf -p + +.LINK +Get-FrecentLocation +Remove-FrecentLocation +Add-Bookmark +Remove-Bookmark +#> + function Set-FrecentLocation { + [OutputType([void])] [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'n')] param( @@ -6,42 +73,63 @@ function Set-FrecentLocation { [ushort] $n = 1, [Parameter(ParameterSetName = 'named', Position = 0)] - [string[]] $NamePart, + [string[]] $Terms, - [Alias("l")] + [Alias('l')] [Parameter(ParameterSetName = 'list', Mandatory)] [switch] $List, [Parameter(ParameterSetName = 'list')] [ushort] $First = $cde.MaxRecentCompletions, [Parameter(ParameterSetName = 'list', ValueFromRemainingArguments)] - [string[]] $Terms, + [string[]] $ListTerms, - [Alias("p")] + [Alias('p')] [Parameter(ParameterSetName = 'prune', Mandatory)] [switch] $Prune, - [Parameter(ParameterSetName = 'prune', Position = 1, Mandatory, ValueFromPipeline)] - [string] $PrunePattern, + [Parameter(ParameterSetName = 'prune', Position = 1, ValueFromPipeline)] + [SupportsWildcards()] + [string] $PrunePattern = $PWD, + + [Alias('m')] + [Parameter(ParameterSetName = 'mark', Mandatory)] + [switch] $Mark, + [Parameter(ParameterSetName = 'mark', Position = 1, ValueFromPipeline)] + [string] $MarkPath = $PWD, + + [Alias('u')] + [Parameter(ParameterSetName = 'unmark', Mandatory)] + [switch] $Unmark, + [Parameter(ParameterSetName = 'unmark', Position = 1, ValueFromPipeline)] + [string] $UnmarkPattern = $PWD, [switch] $PassThru ) if ($PSCmdlet.ParameterSetName -eq 'n' -and $n -ge 1) { - $recents = @(GetFrecent $n $NamePart) + $recents = @(GetFrecent $n) if ($recents.Count -ge $n) { Set-LocationEx $recents[$n - 1] -PassThru:$PassThru } } if ($PSCmdlet.ParameterSetName -eq 'named') { - $recents = @(GetFrecent 1 $NamePart) + $recents = @(GetFrecent 1 $Terms) if ($recents) { Set-LocationEx $recents[0] -PassThru:$PassThru } - elseif ($cde.RecentDirsFallThrough -and $NamePart.Length -eq 1) { Set-LocationEx $NamePart[0] -PassThru:$PassThru } - else { Write-Error "Could not find '$NamePart' in recent locations." -ErrorAction Stop } + elseif ($cde.RecentDirsFallThrough -and $Terms.Length -eq 1) { Set-LocationEx $Terms[0] -PassThru:$PassThru } + else { Write-Error "Could not find '$Terms' in frecent locations." -ErrorAction Stop } } if ($PSCmdlet.ParameterSetName -eq 'list' -and $List) { - Get-FrecentLocation -First $First -Terms $Terms + Get-FrecentLocation -First $First -Terms $ListTerms } if ($PSCmdlet.ParameterSetName -eq 'prune' -and $Prune) { - Remove-RecentLocation -Pattern $PrunePattern + Remove-RecentLocation -Pattern $PrunePattern @args + } + + if ($PSCmdlet.ParameterSetName -eq 'mark') { + Add-Bookmark -Path $MarkPath + } + + if ($PSCmdlet.ParameterSetName -eq 'unmark') { + Remove-Bookmark -Pattern $UnmarkPattern } } diff --git a/cd-extras/public/Set-LocationEx.ps1 b/cd-extras/public/Set-LocationEx.ps1 index 920ebd2..ed88f55 100644 --- a/cd-extras/public/Set-LocationEx.ps1 +++ b/cd-extras/public/Set-LocationEx.ps1 @@ -196,7 +196,7 @@ Function Set-LocationEx { if ($PWD.Path -ne $startLocation) { $redoStack.Clear() $undoStack.Push($startLocation) - SaveRecent $PWD.Path + UpdateRecent $PWD.Path } if ($steppablePipeline) { diff --git a/cd-extras/public/Set-RecentLocation.ps1 b/cd-extras/public/Set-RecentLocation.ps1 index b273313..56a5c86 100644 --- a/cd-extras/public/Set-RecentLocation.ps1 +++ b/cd-extras/public/Set-RecentLocation.ps1 @@ -1,4 +1,55 @@ +<# +.SYNOPSIS +Navigate to a recently used location or work with the recent locations list. + +.PARAMETER n +Navigate to the nth most recent location. + +.PARAMETER Terms +Navigate to the most recent location matching the given terms. This can be a single term or a comma separated +list. The last (or only) term must match the leaf name of a directory in order to be considered a match. + +.PARAMETER List +List the matching recent locations instead of changing directory. Equivalent to the Get-RecentLocation command. +The current directory is always excluded from the list. + +.PARAMETER ListTerms +Terms to matching when listing recent locations. This can be a single term or a comma or space separated list. +The last (or only) term must match the leaf name of a directory in order to be considered a match. + +.PARAMETER First +When listing recent locations, limits the list to the first n matches. + +.PARAMETER Prune +Prune the recent locations list. Equivalent to the Remove-RecentLocation command. + +.PARAMETER PrunePattern +The pattern to match when pruning recent locations. This should either be a leaf name of directories to remove +or a PowerShell wildcard pattern to be matched against the full directory path. ($PWD by default.) + +.EXAMPLE +PS ~> # navigate to the most recent location matching the given terms +PS ~> cdr temp,py +PS C:\temp\python> + +.EXAMPLE +PS ~> # list recent locations matching the given terms +PS ~> cdr -l temp,py + +n Name Path +- ---- ---- +1 python C:\Temp\python + +.EXAMPLE +PS ~> # remove the current directory from the recent locations list. +PS ~> cdr -p + +.LINK +Get-RecentLocation +Remove-RecentLocation +#> function Set-RecentLocation { + [OutputType([void])] [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'n')] param( @@ -8,18 +59,19 @@ function Set-RecentLocation { [Parameter(ParameterSetName = 'named', Position = 0)] [string[]] $NamePart, - [Alias("l")] + [Alias('l')] [Parameter(ParameterSetName = 'list', Mandatory)] [switch] $List, [Parameter(ParameterSetName = 'list')] [ushort] $First = $cde.MaxRecentCompletions, [Parameter(ParameterSetName = 'list', ValueFromRemainingArguments)] - [string[]] $Terms, + [string[]] $ListTerms, - [Alias("p")] + [Alias('p')] [Parameter(ParameterSetName = 'prune', Mandatory)] [switch] $Prune, [Parameter(ParameterSetName = 'prune', Position = 1, Mandatory, ValueFromPipeline)] + [SupportsWildcards()] [string] $PrunePattern, [switch] $PassThru @@ -27,18 +79,18 @@ function Set-RecentLocation { if ($PSCmdlet.ParameterSetName -eq 'n' -and $n -ge 1) { $recents = @(GetRecent $n $NamePart) - if ($recents.Count -ge $n) { Set-LocationEx $recents[$n - 1] } + if ($recents.Count -ge $n) { Set-LocationEx $recents[$n - 1] -PassThru:$PassThru } } if ($PSCmdlet.ParameterSetName -eq 'named') { $recents = @(GetRecent 1 $NamePart) - if ($recents) { Set-LocationEx $recents[0] } - elseif ($cde.RecentDirsFallThrough -and $NamePart.Length -eq 1) { Set-LocationEx $NamePart[0] } + if ($recents) { Set-LocationEx $recents[0] -PassThru:$PassThru } + elseif ($cde.RecentDirsFallThrough -and $NamePart.Length -eq 1) { Set-LocationEx $NamePart[0] -PassThru:$PassThru } else { Write-Error "Could not find '$NamePart' in recent locations." -ErrorAction Stop } } if ($PSCmdlet.ParameterSetName -eq 'list' -and $List) { - Get-RecentLocation -First $First -Terms $Terms + Get-RecentLocation -First $First -Terms $ListTerms } if ($PSCmdlet.ParameterSetName -eq 'prune' -and $Prune) { diff --git a/cd-extras/public/_Classes.ps1 b/cd-extras/public/_Classes.ps1 index d38d73e..34d03cf 100644 --- a/cd-extras/public/_Classes.ps1 +++ b/cd-extras/public/_Classes.ps1 @@ -6,24 +6,33 @@ class IndexedPath { [string] ToString() { return $this.Path } } +class RecentDir { + [string] $Path + [ulong] $LastEntered + [uint] $EnterCount + [bool] $Favour + + [string] ToString() { return "{0}, {1}, {2}" -f $this.LastEntered, $this.Count, $this.Favour } +} + class CdeOptions { - [String[]] $CD_PATH = @() + hidden [string] $recentHash + [bool] $AUTO_CD = $true [bool] $CDABLE_VARS = $false - [String] $NOARG_CD = '~' - [String] $RECENT_DIRS_FILE = $null - [String[]] $RECENT_DIRS_EXCLUDE = @() + [string[]] $CD_PATH = @() + [string] $NOARG_CD = '~' + [string] $RECENT_DIRS_FILE = $null + [string[]] $RECENT_DIRS_EXCLUDE = @() [bool] $RecentDirsFallThrough = $true - [ushort] $MaxRecentDirs = 400 - [ushort] $MaxCompletions = 0 + [ushort] $MaxRecentDirs = 800 [ushort] $MaxRecentCompletions = 100 + [ushort] $MaxCompletions = 0 [ushort] $MaxMenuLength = 36 - [Char[]] $WordDelimiters = '.', '_', '-' - [UInt16] $MaxCompletions = 0 - [UInt16] $MaxMenuLength = 36 - [String[]] $DirCompletions = @('Set-Location', 'Set-LocationEx', 'Push-Location') - [String[]] $PathCompletions = @('Get-ChildItem', 'Get-Item', 'Invoke-Item', 'Expand-Path') - [String[]] $FileCompletions = @() + [char[]] $WordDelimiters = '.', '_', '-' + [string[]] $DirCompletions = @('Set-Location', 'Set-LocationEx', 'Push-Location') + [string[]] $PathCompletions = @('Get-ChildItem', 'Get-Item', 'Invoke-Item', 'Expand-Path') + [string[]] $FileCompletions = @() [bool] $ColorCompletion = $false [bool] $IndexedCompletion = (Get-Module PSReadLine) -and ( Get-PSReadLineKeyHandler -Bound | Where Function -eq MenuComplete @@ -33,12 +42,3 @@ class CdeOptions { $item, "$([char]27)[3m(+additional results not displayed)$([char]27)[0m" } } - -class RecentDir { - [string] $Path - [ulong] $LastEntered - [uint] $EnterCount - [bool] $Starred - - [string] ToString() { return "{0}, {1}" -f $this.LastEntered, $this.Count } -} diff --git a/coverage.xml b/coverage.xml new file mode 100644 index 0000000..3cc6f2b --- /dev/null +++ b/coverage.xml @@ -0,0 +1 @@ + diff --git a/readme.md b/readme.md index beab5fa..e736873 100644 --- a/readme.md +++ b/readme.md @@ -493,9 +493,9 @@ You can extend the list of commands that participate in enhanced completion for directories to get to the file you're looking for.) ```powershell -[~]> setocd DirCompletions mkdir -[~]> mkdir ~/pow/src⇥ -[~]> mkdir ~\powershell\src\█ +[~]> setocd DirCompletions md +[~]> md ~/pow/src⇥ +[~]> md ~\powershell\src\█ [~]> setocd PathCompletions Copy-Item [~]> cp /t/⇥ [~]> cp C:\temp\subdir\█ @@ -651,7 +651,7 @@ with the same name exists in both the current directory and a `CD_PATH` director prefer the former. ```powershell -[~]> mkdir -f child, someDir/child +[~]> md -f child, someDir/child [~]> resolve-path someDir | setocd CD_PATH [~]> cd child [~/child]> cd child @@ -796,7 +796,7 @@ Add-Content $PROFILE `n, 'Import-Module cd-extras' or get the latest from github ```powershell -git clone git@github.com:nickcox/cd-extras.git +git clone https://github.com/nickcox/cd-extras.git Import-Module cd-extras/cd-extras/cd-extras.psd1 # yep, three :D ``` @@ -867,7 +867,7 @@ setocd ToolTip { "$($args[0]) ($($args[0].Mode))" } :point_right: If you want to opt out of the default [completions](#enhanced-completion-for-cd-and-others) then you should do it before _cd-extras_ is loaded since PowerShell doesn't provide a way to -unregister argument completers. +deregister argument completers. ```powershell $cde = @{ DirCompletions = @() } diff --git a/tests/cd-extras.Tests.ps1 b/tests/cd-extras.Tests.ps1 index 5f8ef0c..b0dcf04 100644 --- a/tests/cd-extras.Tests.ps1 +++ b/tests/cd-extras.Tests.ps1 @@ -5,12 +5,18 @@ BeforeDiscovery { $Script:IsWindows = $PSEdition -eq 'desktop' -or $env:OS -like "windows*" } - $Script:xcde = if (Test-Path variable:cde) { $cde } - $Global:cde = $null + Remove-Module cd-extras -ErrorAction Ignore + + # $Script:xcde = if (Test-Path variable:cde) { $cde } + # $Global:cde = [CdeOptions]@{} Push-Location $PSScriptRoot Import-Module ../cd-extras/cd-extras.psd1 -Force } +AfterAll { + Pop-Location +} + Describe 'cd-extras' { BeforeAll { @@ -20,12 +26,6 @@ Describe 'cd-extras' { } } - AfterAll { - Clear-Stack - $Global:cde = $xcde - Pop-Location - } - BeforeEach { Set-Location TestDrive:\ setocd CD_PATH @() @@ -129,7 +129,8 @@ Describe 'cd-extras' { } It 'pops a directory with literal square brackets' { - cd 'powershell/directory`[with`]squarebrackets/one'; cd .. + cd 'powershell/directory`[with`]squarebrackets/one' + cd .. cd- CurrentDir | Should -Be one } @@ -204,23 +205,203 @@ Describe 'cd-extras' { } } - Describe 'Step-Between' { + Describe 'Set-RecentLocation' { + BeforeEach { Remove-RecentLocation * } + It 'toggles between two directories' { - cd ./powershell/src/Modules - cd ../../demos/Apache - cdb - CurrentDir | Should -Be Modules - cdb - CurrentDir | Should -Be Apache + cd /powershell/tools/ResxGen + cd /powershell/tools/terms + cdr + CurrentDir | Should -Be ResxGen + cdr + CurrentDir | Should -Be terms } It 'supports the PassThru switch' { - cd ./powershell/src/Modules - cd ../../demos/Apache - $path = cdb -PassThru - $path | SPlit-Path -Leaf | Should -Be Modules - $path = cdb -PassThru - $path | SPlit-Path -Leaf | Should -Be Apache + cd /powershell/tools/ResxGen + cd /powershell/tools/terms + $path = cdr -PassThru + $path | SPlit-Path -Leaf | Should -Be ResxGen + $path = cdr -PassThru + $path | SPlit-Path -Leaf | Should -Be terms + } + + It 'can move to a recent location by name' { + cd /powershell/tools/ResxGen + cd /powershell/tools/terms + cd / + $thru = cdr tool, resx -PassThru + Get-Location | Split-Path -Leaf | Should -be 'ResxGen' + $thru.Path | Should -BeLike '*ResxGen' + } + + It 'truncates the recent locations list if necessary' { + cd /powershell/tools/failingTests + cd /powershell/tools/packaging + cd /powershell/tools/releaseBuild + cd /powershell/tools/ResxGen + cd /powershell/tools/terms + cd / + (cdr -l).Count | Should -Be 5 + setocd MaxRecentDirs 3 + cd /powershell + (cdr -l).Count | Should -Be 2 + setocd MaxRecentDirs 10 + } + + It 'supports fallthrough' { + setocd RecentDirsFallThrough $true + cdr powershell/tools + Get-Location | Should -BeLike "*powershell${/}tools" + + cd / + Remove-RecentLocation * + setocd RecentDirsFallThrough $false + { cdr powershell/tools } | Should -Throw 'Could not find*' + $Error.Clear() + } + + It 'can list andprune' { + cd /powershell/tools/ResxGen + cd /powershell/tools/terms + cd / + (cdr -l).Count | Should -Be 2 + + cdr -p * + (cdr -l) | Should -BeNullOrEmpty + } + } + + Describe 'Get-RecentLocation' { + BeforeEach { Remove-RecentLocation * } + + It 'returns most recent directories in order' { + cd /powershell/tools/ResxGen + cd /powershell/tools/terms + cd / + Get-RecentLocation | select -Expand Name | Should -Be 'terms', 'ResxGen' + } + + It 'refreshes the list if RECENT_DIRS_FILE updated in another process' { + Get-RecentLocation | Should -BeNullOrEmpty + + $fst, $snd = ((Resolve-Path /), (Resolve-Path /powershell)).Path + $newList = [Collections.Generic.Dictionary[string, RecentDir]]::new() + $newList[$fst] = @{ + Path = $fst + LastEntered = [System.DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() + EnterCount = 1 + } + $newList[$snd] = @{ + Path = $snd + LastEntered = [System.DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() + EnterCount = 1 + } + + setocd RECENT_DIRS_FILE './recent_dirs' + $newList.Values | Export-Csv -LiteralPath $cde.RECENT_DIRS_FILE + + Get-RecentLocation | Should -Not -BeNullOrEmpty + setocd RECENT_DIRS_FILE + } + } + + Describe 'Set-FrecentLocation' { + BeforeEach { Remove-RecentLocation * } + + It 'can move to the nth most frecent location (with passthrough)' { + cdf -l | Should -BeNullOrEmpty + cd /powershell/tools/ResxGen + cd /powershell/tools/terms + cd / + $frecents = cdf -l + $thru = cdf 2 -PassThru + Get-Location | Split-Path -Leaf | Should -be $frecents[1].Name + $thru.Path | Should -BeLike $frecents[1].Path + } + + It 'can move to a frecent location by name' { + cd /powershell/tools/ResxGen + cd /powershell/tools/terms + cd / + $thru = cdf term -PassThru + Get-Location | Split-Path -Leaf | Should -be 'terms' + $thru.Path | Should -BeLike '*terms' + } + + It 'supports fallthrough' { + setocd RecentDirsFallThrough $true + cdf powershell/tools + Get-Location | Should -BeLike "*powershell${/}tools" + + cd / + Remove-RecentLocation * + setocd RecentDirsFallThrough $false + { cdf powershell/tools } | Should -Throw 'Could not find*' + $Error.Clear() + } + + It 'can list, prune and mark' { + cd /powershell/tools/ResxGen + cd /powershell/tools/terms + cd / + (cdf -l).Count | Should -Be 2 + + cdf -p * + (cdf -l) | Should -BeNullOrEmpty + + Get-BookMark | Should -BeNullOrEmpty + cdf -m + Get-Bookmark | Should -Be $PWD.Path + cdf -u + Get-BookMark | Should -BeNullOrEmpty + } + } + + Describe 'Get-FrecentLocation' { + BeforeEach { Remove-RecentLocation * } + + It 'prefers highest ranked directory when last accessed times are similar' { + Get-FrecentLocation | Should -BeNullOrEmpty + cd /powershell/tools/terms + cd /powershell/tools/ResxGen + cd / + cd /powershell/tools/ResxGen + cd / + $actual = Get-FrecentLocation | select -Expand Name + $actual[0] | Should -Be 'resxgen' + $actual[1] | Should -Be 'terms' + } + + It 'prefers bookmarked directory regardless of frecency' { + Get-FrecentLocation | Should -BeNullOrEmpty + cd /powershell/tools/terms + cd /powershell/tools/ResxGen + cd / + cd /powershell/tools/ResxGen + cd / + mark powershell/tools/packaging + $actual = Get-FrecentLocation | select -Expand Name + $actual[0] | Should -Be 'packaging' + $actual[1] | Should -Be 'resxgen' + $actual[2] | Should -Be 'terms' + } + } + + Describe 'Add-Bookmark, Remove-Bookmark' { + BeforeEach { Remove-RecentLocation * } + + It 'can bookmark multiple directories' { + '/powershell/tools/failingTests', + '/powershell/tools/packaging', + '/powershell/tools/releaseBuild', + '/powershell/tools/ResxGen', + '/powershell/tools/terms' | mark + + (Get-Bookmark).Count | Should -Be 5 + + Get-Bookmark | Remove-Bookmark + @(Get-Bookmark).Count | Should -Be 0 } } @@ -238,7 +419,7 @@ Describe 'cd-extras' { } It 'works even when CD_PATH is set' { - setocd CD_PATH @('TestDrive:\powershell\src\') + setocd CD_PATH @('TestDrive:/powershell/src/') Set-Location ./powershell/src/Modules/Shared/Microsoft.PowerShell.Utility cd ... CurrentDir | Should -Be Modules @@ -364,13 +545,13 @@ Describe 'cd-extras' { } It 'throws if the given name part is not found' { - Set-Location powershell\src\Modules\Shared\ + Set-Location powershell/src/Modules/Shared/ { Step-Up zrc } | Should -Throw $Error.Clear() } It 'supports the PassThru switch' { - Set-Location p*\src\Sys*\Format*\common\Utilities + Set-Location p*/src/Sys*/Format*/common/Utilities $path = Step-Up -PassThru $path | Split-Path -Leaf | Should -Be common } @@ -989,6 +1170,37 @@ Describe 'cd-extras' { } } + Describe 'Recents expansion' { + BeforeEach { Remove-RecentLocation * } + + It 'expands recent directories in order' { + cd /powershell/tools/terms + cd /powershell/tools/ResxGen + cd / + $actual = CompleteRecent -wordToComplete '' + $actual[0].CompletionText | Should -BeLike "*resxgen" + $actual[1].CompletionText | Should -BeLike "*terms" + } + + It 'expands recent directories by name' { + cd p*\src\Sys*\Format*\common\Utilities + cd .. + $actual = CompleteRecent -wordToComplete 'Util' + $actual[0].CompletionText | Should -BeLike "*common${/}Utilities"* + } + } + + Describe 'Frecents expansion' { + BeforeEach { Remove-RecentLocation * } + + It 'expands frecent directories by name' { + cd p*\src\Sys*\Format*\common\Utilities + cd .. + $actual = CompleteFrecent -wordToComplete 'Util' + $actual[0].CompletionText | Should -BeLike "*common${/}Utilities"* + } + } + Describe 'Set-CdExtrasOption' { It 'Setting a completion option extends existing completions' { $pathCompletions = $cde.PathCompletions @@ -1008,6 +1220,30 @@ Describe 'cd-extras' { $cde.FileCompletions.Count | Should -Be $originalCount } + + It 'Persists, loads or creates recent dirs file as necessary' { + Remove-RecentLocation * + cd /powershell/tools/terms + cd /powershell/tools/ResxGen + cd / + + setocd RECENT_DIRS_FILE /recent_dirs + Sleep -Milliseconds 10 # save is async + (Get-Content ./recent_dirs).Length | Should -Be 4 # including header + + setocd RECENT_DIRS_FILE + Remove-RecentLocation * + Get-RecentLocation | Should -BeNullOrEmpty + setocd RECENT_DIRS_FILE /recent_dirs + (Get-RecentLocation).Count | Should -Be 2 + + Remove-RecentLocation * + Test-Path /recent_dirs_2 | Should -Be $false + setocd RECENT_DIRS_FILE /recent_dirs_2 + Test-Path /recent_dirs_2 | Should -Be $true + + setocd RECENT_DIRS_FILE + } } Describe 'core' { diff --git a/tests/sampleStructure.txt b/tests/sampleStructure.txt index 150a49c..caa8abb 100644 --- a/tests/sampleStructure.txt +++ b/tests/sampleStructure.txt @@ -1,388 +1,388 @@ powershell -powershell\.git -powershell\.github -powershell\assets -powershell\demos -powershell\docker -powershell\docs -powershell\src -powershell\test -powershell\tools -powershell\.git\hooks -powershell\.git\info -powershell\.git\logs -powershell\.git\objects -powershell\.git\refs -powershell\.git\logs\refs -powershell\.git\logs\refs\heads -powershell\.git\logs\refs\remotes -powershell\.git\logs\refs\remotes\origin -powershell\.git\objects\info -powershell\.git\objects\pack -powershell\.git\refs\heads -powershell\.git\refs\remotes -powershell\.git\refs\tags -powershell\.git\refs\remotes\origin -powershell\.github\Images -powershell\demos\Apache -powershell\demos\Azure -powershell\demos\crontab -powershell\demos\Docker-PowerShell -powershell\demos\DSC -powershell\demos\install -powershell\demos\powershellget -powershell\demos\python -powershell\demos\rest -powershell\demos\SSHRemoting -powershell\demos\SystemD -powershell\demos\WindowsPowerShellModules -powershell\demos\Apache\Apache -powershell\demos\crontab\CronTab -powershell\demos\SystemD\SystemD -powershell\docker\community -powershell\docker\release -powershell\docker\tests -powershell\docker\community\amazonlinux -powershell\docker\community\archlinux -powershell\docker\release\centos7 -powershell\docker\release\fedora26 -powershell\docker\release\fedora27 -powershell\docker\release\nanoserver -powershell\docker\release\opensuse42.2 -powershell\docker\release\ubuntu14.04 -powershell\docker\release\ubuntu16.04 -powershell\docker\release\windowsservercore -powershell\docker\tests\Templates -powershell\docker\tests\Templates\centos7 -powershell\docker\tests\Templates\debian.8 -powershell\docker\tests\Templates\debian.9 -powershell\docker\tests\Templates\fedora26 -powershell\docker\tests\Templates\fedora27 -powershell\docker\tests\Templates\kalilinux -powershell\docker\tests\Templates\opensuse42.2 -powershell\docker\tests\Templates\ubuntu14.04 -powershell\docker\tests\Templates\ubuntu16.04 -powershell\docker\tests\Templates\ubuntu17.04 -powershell\docs\building -powershell\docs\cmdlet-example -powershell\docs\community -powershell\docs\debugging -powershell\docs\dev-process -powershell\docs\git -powershell\docs\host-powershell -powershell\docs\installation -powershell\docs\learning-powershell -powershell\docs\maintainers -powershell\docs\testing-guidelines -powershell\docs\cmdlet-example\Images -powershell\docs\host-powershell\sample-dotnet1.1 -powershell\docs\host-powershell\sample-dotnet2.0-powershell.beta.1 -powershell\docs\host-powershell\sample-dotnet2.0-powershell.beta.3 -powershell\docs\host-powershell\sample-dotnet1.1\Logic -powershell\docs\host-powershell\sample-dotnet1.1\MyApp -powershell\docs\host-powershell\sample-dotnet2.0-powershell.beta.1\Logic -powershell\docs\host-powershell\sample-dotnet2.0-powershell.beta.1\MyApp -powershell\docs\host-powershell\sample-dotnet2.0-powershell.beta.3\MyApp -powershell\docs\testing-guidelines\Images -powershell\src\libpsl-native -powershell\src\Microsoft.Management.Infrastructure.CimCmdlets -powershell\src\Microsoft.PowerShell.Commands.Diagnostics -powershell\src\Microsoft.PowerShell.Commands.Management -powershell\src\Microsoft.PowerShell.Commands.Utility -powershell\src\Microsoft.PowerShell.ConsoleHost -powershell\src\Microsoft.PowerShell.CoreCLR.Eventing -powershell\src\Microsoft.PowerShell.LocalAccounts -powershell\src\Microsoft.PowerShell.PSReadLine -powershell\src\Microsoft.PowerShell.ScheduledJob -powershell\src\Microsoft.PowerShell.SDK -powershell\src\Microsoft.PowerShell.Security -powershell\src\Microsoft.WSMan.Management -powershell\src\Microsoft.WSMan.Runtime -powershell\src\Modules -powershell\src\powershell -powershell\src\powershell-native -powershell\src\powershell-unix -powershell\src\powershell-win-core -powershell\src\PowerShell.Core.Instrumentation -powershell\src\ResGen -powershell\src\Schemas -powershell\src\signing -powershell\src\System.Management.Automation -powershell\src\TypeCatalogGen -powershell\src\libpsl-native\src -powershell\src\libpsl-native\test -powershell\src\libpsl-native\test\googletest -powershell\src\Microsoft.Management.Infrastructure.CimCmdlets\resources -powershell\src\Microsoft.PowerShell.Commands.Diagnostics\CoreCLR -powershell\src\Microsoft.PowerShell.Commands.Diagnostics\resources -powershell\src\Microsoft.PowerShell.Commands.Management\cimSupport -powershell\src\Microsoft.PowerShell.Commands.Management\commands -powershell\src\Microsoft.PowerShell.Commands.Management\resources -powershell\src\Microsoft.PowerShell.Commands.Management\singleshell -powershell\src\Microsoft.PowerShell.Commands.Management\cimSupport\cmdletization -powershell\src\Microsoft.PowerShell.Commands.Management\cimSupport\cmdletization\cim -powershell\src\Microsoft.PowerShell.Commands.Management\commands\management -powershell\src\Microsoft.PowerShell.Commands.Management\singleshell\installer -powershell\src\Microsoft.PowerShell.Commands.Utility\commands -powershell\src\Microsoft.PowerShell.Commands.Utility\resources -powershell\src\Microsoft.PowerShell.Commands.Utility\singleshell -powershell\src\Microsoft.PowerShell.Commands.Utility\commands\utility -powershell\src\Microsoft.PowerShell.Commands.Utility\commands\utility\FormatAndOutput -powershell\src\Microsoft.PowerShell.Commands.Utility\commands\utility\ShowCommand -powershell\src\Microsoft.PowerShell.Commands.Utility\commands\utility\trace -powershell\src\Microsoft.PowerShell.Commands.Utility\commands\utility\WebCmdlet -powershell\src\Microsoft.PowerShell.Commands.Utility\commands\utility\FormatAndOutput\common -powershell\src\Microsoft.PowerShell.Commands.Utility\commands\utility\FormatAndOutput\format-hex -powershell\src\Microsoft.PowerShell.Commands.Utility\commands\utility\FormatAndOutput\format-list -powershell\src\Microsoft.PowerShell.Commands.Utility\commands\utility\FormatAndOutput\format-object -powershell\src\Microsoft.PowerShell.Commands.Utility\commands\utility\FormatAndOutput\format-table -powershell\src\Microsoft.PowerShell.Commands.Utility\commands\utility\FormatAndOutput\format-wide -powershell\src\Microsoft.PowerShell.Commands.Utility\commands\utility\FormatAndOutput\out-file -powershell\src\Microsoft.PowerShell.Commands.Utility\commands\utility\FormatAndOutput\out-printer -powershell\src\Microsoft.PowerShell.Commands.Utility\commands\utility\FormatAndOutput\out-string -powershell\src\Microsoft.PowerShell.Commands.Utility\commands\utility\FormatAndOutput\OutGridView -powershell\src\Microsoft.PowerShell.Commands.Utility\commands\utility\WebCmdlet\Common -powershell\src\Microsoft.PowerShell.Commands.Utility\commands\utility\WebCmdlet\CoreCLR -powershell\src\Microsoft.PowerShell.Commands.Utility\singleshell\installer -powershell\src\Microsoft.PowerShell.ConsoleHost\host -powershell\src\Microsoft.PowerShell.ConsoleHost\resources -powershell\src\Microsoft.PowerShell.ConsoleHost\singleshell -powershell\src\Microsoft.PowerShell.ConsoleHost\host\msh -powershell\src\Microsoft.PowerShell.ConsoleHost\singleshell\installer -powershell\src\Microsoft.PowerShell.CoreCLR.Eventing\DotNetCode -powershell\src\Microsoft.PowerShell.CoreCLR.Eventing\resources -powershell\src\Microsoft.PowerShell.CoreCLR.Eventing\DotNetCode\Eventing -powershell\src\Microsoft.PowerShell.CoreCLR.Eventing\DotNetCode\Eventing\Reader -powershell\src\Microsoft.PowerShell.LocalAccounts\LocalAccounts -powershell\src\Microsoft.PowerShell.LocalAccounts\resources -powershell\src\Microsoft.PowerShell.LocalAccounts\LocalAccounts\Commands -powershell\src\Microsoft.PowerShell.PSReadLine\en-US -powershell\src\Microsoft.PowerShell.ScheduledJob\commands -powershell\src\Microsoft.PowerShell.ScheduledJob\resources -powershell\src\Microsoft.PowerShell.Security\resources -powershell\src\Microsoft.PowerShell.Security\security -powershell\src\Microsoft.PowerShell.Security\singleshell -powershell\src\Microsoft.PowerShell.Security\singleshell\installer -powershell\src\Microsoft.WSMan.Management\resources -powershell\src\Modules\Shared -powershell\src\Modules\Unix -powershell\src\Modules\Windows -powershell\src\Modules\Windows-Core -powershell\src\Modules\Windows-Core+Full -powershell\src\Modules\Windows-Full -powershell\src\Modules\Shared\Microsoft.PowerShell.Host -powershell\src\Modules\Shared\Microsoft.PowerShell.Utility -powershell\src\Modules\Shared\PSReadLine -powershell\src\Modules\Unix\Microsoft.PowerShell.Management -powershell\src\Modules\Unix\Microsoft.PowerShell.Security -powershell\src\Modules\Unix\Microsoft.PowerShell.Utility -powershell\src\Modules\Windows-Core\Microsoft.PowerShell.Diagnostics -powershell\src\Modules\Windows-Core\Microsoft.PowerShell.Management -powershell\src\Modules\Windows-Core\Microsoft.PowerShell.Utility -powershell\src\Modules\Windows-Core+Full\CimCmdlets -powershell\src\Modules\Windows-Core+Full\Microsoft.PowerShell.Security -powershell\src\Modules\Windows-Core+Full\Microsoft.WSMan.Management -powershell\src\Modules\Windows-Core+Full\PSDiagnostics -powershell\src\Modules\Windows-Full\Microsoft.PowerShell.Diagnostics -powershell\src\Modules\Windows-Full\Microsoft.PowerShell.LocalAccounts -powershell\src\Modules\Windows-Full\Microsoft.PowerShell.Management -powershell\src\Modules\Windows-Full\Microsoft.PowerShell.ODataUtils -powershell\src\Modules\Windows-Full\Microsoft.PowerShell.Utility -powershell\src\Modules\Windows-Full\PSScheduledJob -powershell\src\Modules\Windows-Full\Microsoft.PowerShell.ODataUtils\en-US -powershell\src\powershell-native\nativemsh -powershell\src\powershell-native\nativemsh\pwrshcommon -powershell\src\powershell-native\nativemsh\pwrshexe -powershell\src\powershell-native\nativemsh\pwrshplugin -powershell\src\Schemas\PSMaml -powershell\src\System.Management.Automation\cimSupport -powershell\src\System.Management.Automation\CoreCLR -powershell\src\System.Management.Automation\DscSupport -powershell\src\System.Management.Automation\engine -powershell\src\System.Management.Automation\FormatAndOutput -powershell\src\System.Management.Automation\help -powershell\src\System.Management.Automation\logging -powershell\src\System.Management.Automation\namespaces -powershell\src\System.Management.Automation\resources -powershell\src\System.Management.Automation\security -powershell\src\System.Management.Automation\singleshell -powershell\src\System.Management.Automation\utils -powershell\src\System.Management.Automation\cimSupport\cmdletization -powershell\src\System.Management.Automation\cimSupport\other -powershell\src\System.Management.Automation\cimSupport\cmdletization\cim -powershell\src\System.Management.Automation\cimSupport\cmdletization\xml -powershell\src\System.Management.Automation\cimSupport\cmdletization\xml\CoreCLR -powershell\src\System.Management.Automation\engine\COM -powershell\src\System.Management.Automation\engine\ComInterop -powershell\src\System.Management.Automation\engine\CommandCompletion -powershell\src\System.Management.Automation\engine\debugger -powershell\src\System.Management.Automation\engine\hostifaces -powershell\src\System.Management.Automation\engine\interpreter -powershell\src\System.Management.Automation\engine\lang -powershell\src\System.Management.Automation\engine\Modules -powershell\src\System.Management.Automation\engine\parser -powershell\src\System.Management.Automation\engine\remoting -powershell\src\System.Management.Automation\engine\runtime -powershell\src\System.Management.Automation\engine\WinRT -powershell\src\System.Management.Automation\engine\lang\interface -powershell\src\System.Management.Automation\engine\remoting\client -powershell\src\System.Management.Automation\engine\remoting\commands -powershell\src\System.Management.Automation\engine\remoting\common -powershell\src\System.Management.Automation\engine\remoting\fanin -powershell\src\System.Management.Automation\engine\remoting\host -powershell\src\System.Management.Automation\engine\remoting\server -powershell\src\System.Management.Automation\engine\remoting\common\WireDataFormat -powershell\src\System.Management.Automation\engine\runtime\Binding -powershell\src\System.Management.Automation\engine\runtime\Operations -powershell\src\System.Management.Automation\FormatAndOutput\common -powershell\src\System.Management.Automation\FormatAndOutput\DefaultFormatters -powershell\src\System.Management.Automation\FormatAndOutput\format-default -powershell\src\System.Management.Automation\FormatAndOutput\out-console -powershell\src\System.Management.Automation\FormatAndOutput\out-textInterface -powershell\src\System.Management.Automation\FormatAndOutput\common\DisplayDatabase -powershell\src\System.Management.Automation\FormatAndOutput\common\Utilities -powershell\src\System.Management.Automation\logging\eventlog -powershell\src\System.Management.Automation\singleshell\config -powershell\src\System.Management.Automation\utils\perfCounters -powershell\src\System.Management.Automation\utils\tracing -powershell\test\common -powershell\test\csharp -powershell\test\docker -powershell\test\powershell -powershell\test\PSReadLine -powershell\test\shebang -powershell\test\tools -powershell\test\common\markdown -powershell\test\docker\networktest -powershell\test\powershell\engine -powershell\test\powershell\Host -powershell\test\powershell\Installer -powershell\test\powershell\Language -powershell\test\powershell\Modules -powershell\test\powershell\Provider -powershell\test\powershell\SDK -powershell\test\powershell\engine\Api -powershell\test\powershell\engine\Basic -powershell\test\powershell\engine\Cdxml -powershell\test\powershell\engine\COM -powershell\test\powershell\engine\ETS -powershell\test\powershell\engine\Help -powershell\test\powershell\engine\Job -powershell\test\powershell\engine\Module -powershell\test\powershell\engine\ParameterBinding -powershell\test\powershell\engine\Remoting -powershell\test\powershell\engine\ResourceValidation -powershell\test\powershell\engine\Api\assets -powershell\test\powershell\engine\Cdxml\assets -powershell\test\powershell\engine\Cdxml\assets\CimTest -powershell\test\powershell\engine\Help\assets -powershell\test\powershell\engine\Help\assets\HelpURI -powershell\test\powershell\engine\Module\assets -powershell\test\powershell\engine\Module\assets\testmodulerunspace -powershell\test\powershell\engine\Module\assets\testmodulerunspace\ModuleWithDependencies2 -powershell\test\powershell\engine\Module\assets\testmodulerunspace\NestedRequiredModule1 -powershell\test\powershell\engine\Module\assets\testmodulerunspace\ModuleWithDependencies2\2.0 -powershell\test\powershell\engine\Module\assets\testmodulerunspace\NestedRequiredModule1\2.5 -powershell\test\powershell\Host\assets -powershell\test\powershell\Host\TabCompletion -powershell\test\powershell\Language\Classes -powershell\test\powershell\Language\Interop -powershell\test\powershell\Language\Operators -powershell\test\powershell\Language\Parser -powershell\test\powershell\Language\Scripting -powershell\test\powershell\Language\Interop\DotNet -powershell\test\powershell\Language\Scripting\Debugging -powershell\test\powershell\Language\Scripting\en-US -powershell\test\powershell\Language\Scripting\fr-FR -powershell\test\powershell\Language\Scripting\NativeExecution -powershell\test\powershell\Language\Scripting\newbase -powershell\test\powershell\Language\Scripting\newbase\en-US -powershell\test\powershell\Language\Scripting\newbase\fr-FR -powershell\test\powershell\Modules\CimCmdlets -powershell\test\powershell\Modules\Microsoft.PowerShell.Core -powershell\test\powershell\Modules\Microsoft.PowerShell.Diagnostics -powershell\test\powershell\Modules\Microsoft.Powershell.Host -powershell\test\powershell\Modules\Microsoft.PowerShell.LocalAccounts -powershell\test\powershell\Modules\Microsoft.PowerShell.Management -powershell\test\powershell\Modules\Microsoft.PowerShell.Security -powershell\test\powershell\Modules\Microsoft.PowerShell.Utility -powershell\test\powershell\Modules\Microsoft.WSMan.Management -powershell\test\powershell\Modules\PackageManagement -powershell\test\powershell\Modules\PowerShellGet -powershell\test\powershell\Modules\PSDesiredStateConfiguration -powershell\test\powershell\Modules\PSReadLine -powershell\test\powershell\Modules\Microsoft.PowerShell.Diagnostics\assets -powershell\test\powershell\Modules\Microsoft.PowerShell.Management\assets -powershell\test\powershell\Modules\Microsoft.PowerShell.Security\TestData -powershell\test\powershell\Modules\Microsoft.PowerShell.Security\TestData\CatalogTestData -powershell\test\powershell\Modules\Microsoft.PowerShell.Security\TestData\CatalogTestData\UserConfigProv -powershell\test\powershell\Modules\Microsoft.PowerShell.Security\TestData\CatalogTestData\UserConfigProv\DSCResources -powershell\test\powershell\Modules\Microsoft.PowerShell.Security\TestData\CatalogTestData\UserConfigProv\DSCResources\scriptdsc -powershell\test\powershell\Modules\Microsoft.PowerShell.Security\TestData\CatalogTestData\UserConfigProv\DSCResources\UserConfigProviderModVersion1 -powershell\test\powershell\Modules\Microsoft.PowerShell.Security\TestData\CatalogTestData\UserConfigProv\DSCResources\UserConfigProviderModVersion2 -powershell\test\powershell\Modules\Microsoft.PowerShell.Security\TestData\CatalogTestData\UserConfigProv\DSCResources\UserConfigProviderModVersion3 -powershell\test\powershell\Modules\Microsoft.PowerShell.Utility\assets -powershell\test\powershell\Modules\Microsoft.PowerShell.Utility\assets\de-DE -powershell\test\powershell\Modules\Microsoft.PowerShell.Utility\assets\en-US -powershell\test\powershell\Modules\PSDesiredStateConfiguration\assets -powershell\test\powershell\Modules\PSDesiredStateConfiguration\assets\dsc -powershell\test\powershell\Modules\PSDesiredStateConfiguration\assets\dsc\baseregistration -powershell\test\powershell\Modules\PSDesiredStateConfiguration\assets\dsc\schema -powershell\test\powershell\Modules\PSDesiredStateConfiguration\assets\dsc\schema\MSFT_LogResource -powershell\test\powershell\Modules\PSDesiredStateConfiguration\assets\dsc\schema\MSFT_nxArchiveResource -powershell\test\powershell\Modules\PSDesiredStateConfiguration\assets\dsc\schema\MSFT_nxEnvironmentResource -powershell\test\powershell\Modules\PSDesiredStateConfiguration\assets\dsc\schema\MSFT_nxFileLineResource -powershell\test\powershell\Modules\PSDesiredStateConfiguration\assets\dsc\schema\MSFT_nxFileResource -powershell\test\powershell\Modules\PSDesiredStateConfiguration\assets\dsc\schema\MSFT_nxGroupResource -powershell\test\powershell\Modules\PSDesiredStateConfiguration\assets\dsc\schema\MSFT_nxPackageResource -powershell\test\powershell\Modules\PSDesiredStateConfiguration\assets\dsc\schema\MSFT_nxScriptResource -powershell\test\powershell\Modules\PSDesiredStateConfiguration\assets\dsc\schema\MSFT_nxServiceResource -powershell\test\powershell\Modules\PSDesiredStateConfiguration\assets\dsc\schema\MSFT_nxSshAuthorizedKeysResource -powershell\test\powershell\Modules\PSDesiredStateConfiguration\assets\dsc\schema\MSFT_nxUserResource -powershell\test\powershell\SDK\assets -powershell\test\tools\CodeCoverageAutomation -powershell\test\tools\Modules -powershell\test\tools\OpenCover -powershell\test\tools\TestExe -powershell\test\tools\WebListener -powershell\test\tools\Modules\HelpersCommon -powershell\test\tools\Modules\HelpersHostCS -powershell\test\tools\Modules\HelpersLanguage -powershell\test\tools\Modules\HelpersRemoting -powershell\test\tools\Modules\HttpListener -powershell\test\tools\Modules\WebListener -powershell\test\tools\WebListener\Controllers -powershell\test\tools\WebListener\Models -powershell\test\tools\WebListener\Views -powershell\test\tools\WebListener\Views\Encoding -powershell\test\tools\WebListener\Views\Home -powershell\test\tools\WebListener\Views\Multipart -powershell\test\tools\WebListener\Views\Redirect -powershell\test\tools\WebListener\Views\Shared -powershell\tools\credScan -powershell\tools\failingTests -powershell\tools\packaging -powershell\tools\releaseBuild -powershell\tools\ResxGen -powershell\tools\terms -powershell\tools\packaging\macos -powershell\tools\packaging\project -powershell\tools\packaging\macos\launcher -powershell\tools\packaging\macos\launcher\ROOT -powershell\tools\packaging\macos\launcher\ROOT\Applications -powershell\tools\packaging\macos\launcher\ROOT\Applications\PowerShell.app -powershell\tools\packaging\macos\launcher\ROOT\Applications\PowerShell.app\Contents -powershell\tools\packaging\macos\launcher\ROOT\Applications\PowerShell.app\Contents\MacOS -powershell\tools\releaseBuild\Images -powershell\tools\releaseBuild\macOS -powershell\tools\releaseBuild\Images\GenericLinuxFiles -powershell\tools\releaseBuild\Images\microsoft_powershell_centos7 -powershell\tools\releaseBuild\Images\microsoft_powershell_ubuntu14.04 -powershell\tools\releaseBuild\Images\microsoft_powershell_ubuntu16.04 -powershell\tools\releaseBuild\Images\microsoft_powershell_windowsservercore -powershell\directory with spaces\child one -powershell\directory with spaces\child two -powershell\directory[with]squarebrackets\one -powershell\directory[with]squarebrackets\two -powershell\powershell -powershell\reallyreallyreallyreallyreallreallyreallyreally_longdirectoryname +powershell/.git +powershell/.github +powershell/assets +powershell/demos +powershell/docker +powershell/docs +powershell/src +powershell/test +powershell/tools +powershell/.git/hooks +powershell/.git/info +powershell/.git/logs +powershell/.git/objects +powershell/.git/refs +powershell/.git/logs/refs +powershell/.git/logs/refs/heads +powershell/.git/logs/refs/remotes +powershell/.git/logs/refs/remotes/origin +powershell/.git/objects/info +powershell/.git/objects/pack +powershell/.git/refs/heads +powershell/.git/refs/remotes +powershell/.git/refs/tags +powershell/.git/refs/remotes/origin +powershell/.github/Images +powershell/demos/Apache +powershell/demos/Azure +powershell/demos/crontab +powershell/demos/Docker-PowerShell +powershell/demos/DSC +powershell/demos/install +powershell/demos/powershellget +powershell/demos/python +powershell/demos/rest +powershell/demos/SSHRemoting +powershell/demos/SystemD +powershell/demos/WindowsPowerShellModules +powershell/demos/Apache/Apache +powershell/demos/crontab/CronTab +powershell/demos/SystemD/SystemD +powershell/docker/community +powershell/docker/release +powershell/docker/tests +powershell/docker/community/amazonlinux +powershell/docker/community/archlinux +powershell/docker/release/centos7 +powershell/docker/release/fedora26 +powershell/docker/release/fedora27 +powershell/docker/release/nanoserver +powershell/docker/release/opensuse42.2 +powershell/docker/release/ubuntu14.04 +powershell/docker/release/ubuntu16.04 +powershell/docker/release/windowsservercore +powershell/docker/tests/Templates +powershell/docker/tests/Templates/centos7 +powershell/docker/tests/Templates/debian.8 +powershell/docker/tests/Templates/debian.9 +powershell/docker/tests/Templates/fedora26 +powershell/docker/tests/Templates/fedora27 +powershell/docker/tests/Templates/kalilinux +powershell/docker/tests/Templates/opensuse42.2 +powershell/docker/tests/Templates/ubuntu14.04 +powershell/docker/tests/Templates/ubuntu16.04 +powershell/docker/tests/Templates/ubuntu17.04 +powershell/docs/building +powershell/docs/cmdlet-example +powershell/docs/community +powershell/docs/debugging +powershell/docs/dev-process +powershell/docs/git +powershell/docs/host-powershell +powershell/docs/installation +powershell/docs/learning-powershell +powershell/docs/maintainers +powershell/docs/testing-guidelines +powershell/docs/cmdlet-example/Images +powershell/docs/host-powershell/sample-dotnet1.1 +powershell/docs/host-powershell/sample-dotnet2.0-powershell.beta.1 +powershell/docs/host-powershell/sample-dotnet2.0-powershell.beta.3 +powershell/docs/host-powershell/sample-dotnet1.1/Logic +powershell/docs/host-powershell/sample-dotnet1.1/MyApp +powershell/docs/host-powershell/sample-dotnet2.0-powershell.beta.1/Logic +powershell/docs/host-powershell/sample-dotnet2.0-powershell.beta.1/MyApp +powershell/docs/host-powershell/sample-dotnet2.0-powershell.beta.3/MyApp +powershell/docs/testing-guidelines/Images +powershell/src/libpsl-native +powershell/src/Microsoft.Management.Infrastructure.CimCmdlets +powershell/src/Microsoft.PowerShell.Commands.Diagnostics +powershell/src/Microsoft.PowerShell.Commands.Management +powershell/src/Microsoft.PowerShell.Commands.Utility +powershell/src/Microsoft.PowerShell.ConsoleHost +powershell/src/Microsoft.PowerShell.CoreCLR.Eventing +powershell/src/Microsoft.PowerShell.LocalAccounts +powershell/src/Microsoft.PowerShell.PSReadLine +powershell/src/Microsoft.PowerShell.ScheduledJob +powershell/src/Microsoft.PowerShell.SDK +powershell/src/Microsoft.PowerShell.Security +powershell/src/Microsoft.WSMan.Management +powershell/src/Microsoft.WSMan.Runtime +powershell/src/Modules +powershell/src/powershell +powershell/src/powershell-native +powershell/src/powershell-unix +powershell/src/powershell-win-core +powershell/src/PowerShell.Core.Instrumentation +powershell/src/ResGen +powershell/src/Schemas +powershell/src/signing +powershell/src/System.Management.Automation +powershell/src/TypeCatalogGen +powershell/src/libpsl-native/src +powershell/src/libpsl-native/test +powershell/src/libpsl-native/test/googletest +powershell/src/Microsoft.Management.Infrastructure.CimCmdlets/resources +powershell/src/Microsoft.PowerShell.Commands.Diagnostics/CoreCLR +powershell/src/Microsoft.PowerShell.Commands.Diagnostics/resources +powershell/src/Microsoft.PowerShell.Commands.Management/cimSupport +powershell/src/Microsoft.PowerShell.Commands.Management/commands +powershell/src/Microsoft.PowerShell.Commands.Management/resources +powershell/src/Microsoft.PowerShell.Commands.Management/singleshell +powershell/src/Microsoft.PowerShell.Commands.Management/cimSupport/cmdletization +powershell/src/Microsoft.PowerShell.Commands.Management/cimSupport/cmdletization/cim +powershell/src/Microsoft.PowerShell.Commands.Management/commands/management +powershell/src/Microsoft.PowerShell.Commands.Management/singleshell/installer +powershell/src/Microsoft.PowerShell.Commands.Utility/commands +powershell/src/Microsoft.PowerShell.Commands.Utility/resources +powershell/src/Microsoft.PowerShell.Commands.Utility/singleshell +powershell/src/Microsoft.PowerShell.Commands.Utility/commands/utility +powershell/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput +powershell/src/Microsoft.PowerShell.Commands.Utility/commands/utility/ShowCommand +powershell/src/Microsoft.PowerShell.Commands.Utility/commands/utility/trace +powershell/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet +powershell/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/common +powershell/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/format-hex +powershell/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/format-list +powershell/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/format-object +powershell/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/format-table +powershell/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/format-wide +powershell/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/out-file +powershell/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/out-printer +powershell/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/out-string +powershell/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/OutGridView +powershell/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common +powershell/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/CoreCLR +powershell/src/Microsoft.PowerShell.Commands.Utility/singleshell/installer +powershell/src/Microsoft.PowerShell.ConsoleHost/host +powershell/src/Microsoft.PowerShell.ConsoleHost/resources +powershell/src/Microsoft.PowerShell.ConsoleHost/singleshell +powershell/src/Microsoft.PowerShell.ConsoleHost/host/msh +powershell/src/Microsoft.PowerShell.ConsoleHost/singleshell/installer +powershell/src/Microsoft.PowerShell.CoreCLR.Eventing/DotNetCode +powershell/src/Microsoft.PowerShell.CoreCLR.Eventing/resources +powershell/src/Microsoft.PowerShell.CoreCLR.Eventing/DotNetCode/Eventing +powershell/src/Microsoft.PowerShell.CoreCLR.Eventing/DotNetCode/Eventing/Reader +powershell/src/Microsoft.PowerShell.LocalAccounts/LocalAccounts +powershell/src/Microsoft.PowerShell.LocalAccounts/resources +powershell/src/Microsoft.PowerShell.LocalAccounts/LocalAccounts/Commands +powershell/src/Microsoft.PowerShell.PSReadLine/en-US +powershell/src/Microsoft.PowerShell.ScheduledJob/commands +powershell/src/Microsoft.PowerShell.ScheduledJob/resources +powershell/src/Microsoft.PowerShell.Security/resources +powershell/src/Microsoft.PowerShell.Security/security +powershell/src/Microsoft.PowerShell.Security/singleshell +powershell/src/Microsoft.PowerShell.Security/singleshell/installer +powershell/src/Microsoft.WSMan.Management/resources +powershell/src/Modules/Shared +powershell/src/Modules/Unix +powershell/src/Modules/Windows +powershell/src/Modules/Windows-Core +powershell/src/Modules/Windows-Core+Full +powershell/src/Modules/Windows-Full +powershell/src/Modules/Shared/Microsoft.PowerShell.Host +powershell/src/Modules/Shared/Microsoft.PowerShell.Utility +powershell/src/Modules/Shared/PSReadLine +powershell/src/Modules/Unix/Microsoft.PowerShell.Management +powershell/src/Modules/Unix/Microsoft.PowerShell.Security +powershell/src/Modules/Unix/Microsoft.PowerShell.Utility +powershell/src/Modules/Windows-Core/Microsoft.PowerShell.Diagnostics +powershell/src/Modules/Windows-Core/Microsoft.PowerShell.Management +powershell/src/Modules/Windows-Core/Microsoft.PowerShell.Utility +powershell/src/Modules/Windows-Core+Full/CimCmdlets +powershell/src/Modules/Windows-Core+Full/Microsoft.PowerShell.Security +powershell/src/Modules/Windows-Core+Full/Microsoft.WSMan.Management +powershell/src/Modules/Windows-Core+Full/PSDiagnostics +powershell/src/Modules/Windows-Full/Microsoft.PowerShell.Diagnostics +powershell/src/Modules/Windows-Full/Microsoft.PowerShell.LocalAccounts +powershell/src/Modules/Windows-Full/Microsoft.PowerShell.Management +powershell/src/Modules/Windows-Full/Microsoft.PowerShell.ODataUtils +powershell/src/Modules/Windows-Full/Microsoft.PowerShell.Utility +powershell/src/Modules/Windows-Full/PSScheduledJob +powershell/src/Modules/Windows-Full/Microsoft.PowerShell.ODataUtils/en-US +powershell/src/powershell-native/nativemsh +powershell/src/powershell-native/nativemsh/pwrshcommon +powershell/src/powershell-native/nativemsh/pwrshexe +powershell/src/powershell-native/nativemsh/pwrshplugin +powershell/src/Schemas/PSMaml +powershell/src/System.Management.Automation/cimSupport +powershell/src/System.Management.Automation/CoreCLR +powershell/src/System.Management.Automation/DscSupport +powershell/src/System.Management.Automation/engine +powershell/src/System.Management.Automation/FormatAndOutput +powershell/src/System.Management.Automation/help +powershell/src/System.Management.Automation/logging +powershell/src/System.Management.Automation/namespaces +powershell/src/System.Management.Automation/resources +powershell/src/System.Management.Automation/security +powershell/src/System.Management.Automation/singleshell +powershell/src/System.Management.Automation/utils +powershell/src/System.Management.Automation/cimSupport/cmdletization +powershell/src/System.Management.Automation/cimSupport/other +powershell/src/System.Management.Automation/cimSupport/cmdletization/cim +powershell/src/System.Management.Automation/cimSupport/cmdletization/xml +powershell/src/System.Management.Automation/cimSupport/cmdletization/xml/CoreCLR +powershell/src/System.Management.Automation/engine/COM +powershell/src/System.Management.Automation/engine/ComInterop +powershell/src/System.Management.Automation/engine/CommandCompletion +powershell/src/System.Management.Automation/engine/debugger +powershell/src/System.Management.Automation/engine/hostifaces +powershell/src/System.Management.Automation/engine/interpreter +powershell/src/System.Management.Automation/engine/lang +powershell/src/System.Management.Automation/engine/Modules +powershell/src/System.Management.Automation/engine/parser +powershell/src/System.Management.Automation/engine/remoting +powershell/src/System.Management.Automation/engine/runtime +powershell/src/System.Management.Automation/engine/WinRT +powershell/src/System.Management.Automation/engine/lang/interface +powershell/src/System.Management.Automation/engine/remoting/client +powershell/src/System.Management.Automation/engine/remoting/commands +powershell/src/System.Management.Automation/engine/remoting/common +powershell/src/System.Management.Automation/engine/remoting/fanin +powershell/src/System.Management.Automation/engine/remoting/host +powershell/src/System.Management.Automation/engine/remoting/server +powershell/src/System.Management.Automation/engine/remoting/common/WireDataFormat +powershell/src/System.Management.Automation/engine/runtime/Binding +powershell/src/System.Management.Automation/engine/runtime/Operations +powershell/src/System.Management.Automation/FormatAndOutput/common +powershell/src/System.Management.Automation/FormatAndOutput/DefaultFormatters +powershell/src/System.Management.Automation/FormatAndOutput/format-default +powershell/src/System.Management.Automation/FormatAndOutput/out-console +powershell/src/System.Management.Automation/FormatAndOutput/out-textInterface +powershell/src/System.Management.Automation/FormatAndOutput/common/DisplayDatabase +powershell/src/System.Management.Automation/FormatAndOutput/common/Utilities +powershell/src/System.Management.Automation/logging/eventlog +powershell/src/System.Management.Automation/singleshell/config +powershell/src/System.Management.Automation/utils/perfCounters +powershell/src/System.Management.Automation/utils/tracing +powershell/test/common +powershell/test/csharp +powershell/test/docker +powershell/test/powershell +powershell/test/PSReadLine +powershell/test/shebang +powershell/test/tools +powershell/test/common/markdown +powershell/test/docker/networktest +powershell/test/powershell/engine +powershell/test/powershell/Host +powershell/test/powershell/Installer +powershell/test/powershell/Language +powershell/test/powershell/Modules +powershell/test/powershell/Provider +powershell/test/powershell/SDK +powershell/test/powershell/engine/Api +powershell/test/powershell/engine/Basic +powershell/test/powershell/engine/Cdxml +powershell/test/powershell/engine/COM +powershell/test/powershell/engine/ETS +powershell/test/powershell/engine/Help +powershell/test/powershell/engine/Job +powershell/test/powershell/engine/Module +powershell/test/powershell/engine/ParameterBinding +powershell/test/powershell/engine/Remoting +powershell/test/powershell/engine/ResourceValidation +powershell/test/powershell/engine/Api/assets +powershell/test/powershell/engine/Cdxml/assets +powershell/test/powershell/engine/Cdxml/assets/CimTest +powershell/test/powershell/engine/Help/assets +powershell/test/powershell/engine/Help/assets/HelpURI +powershell/test/powershell/engine/Module/assets +powershell/test/powershell/engine/Module/assets/testmodulerunspace +powershell/test/powershell/engine/Module/assets/testmodulerunspace/ModuleWithDependencies2 +powershell/test/powershell/engine/Module/assets/testmodulerunspace/NestedRequiredModule1 +powershell/test/powershell/engine/Module/assets/testmodulerunspace/ModuleWithDependencies2/2.0 +powershell/test/powershell/engine/Module/assets/testmodulerunspace/NestedRequiredModule1/2.5 +powershell/test/powershell/Host/assets +powershell/test/powershell/Host/TabCompletion +powershell/test/powershell/Language/Classes +powershell/test/powershell/Language/Interop +powershell/test/powershell/Language/Operators +powershell/test/powershell/Language/Parser +powershell/test/powershell/Language/Scripting +powershell/test/powershell/Language/Interop/DotNet +powershell/test/powershell/Language/Scripting/Debugging +powershell/test/powershell/Language/Scripting/en-US +powershell/test/powershell/Language/Scripting/fr-FR +powershell/test/powershell/Language/Scripting/NativeExecution +powershell/test/powershell/Language/Scripting/newbase +powershell/test/powershell/Language/Scripting/newbase/en-US +powershell/test/powershell/Language/Scripting/newbase/fr-FR +powershell/test/powershell/Modules/CimCmdlets +powershell/test/powershell/Modules/Microsoft.PowerShell.Core +powershell/test/powershell/Modules/Microsoft.PowerShell.Diagnostics +powershell/test/powershell/Modules/Microsoft.Powershell.Host +powershell/test/powershell/Modules/Microsoft.PowerShell.LocalAccounts +powershell/test/powershell/Modules/Microsoft.PowerShell.Management +powershell/test/powershell/Modules/Microsoft.PowerShell.Security +powershell/test/powershell/Modules/Microsoft.PowerShell.Utility +powershell/test/powershell/Modules/Microsoft.WSMan.Management +powershell/test/powershell/Modules/PackageManagement +powershell/test/powershell/Modules/PowerShellGet +powershell/test/powershell/Modules/PSDesiredStateConfiguration +powershell/test/powershell/Modules/PSReadLine +powershell/test/powershell/Modules/Microsoft.PowerShell.Diagnostics/assets +powershell/test/powershell/Modules/Microsoft.PowerShell.Management/assets +powershell/test/powershell/Modules/Microsoft.PowerShell.Security/TestData +powershell/test/powershell/Modules/Microsoft.PowerShell.Security/TestData/CatalogTestData +powershell/test/powershell/Modules/Microsoft.PowerShell.Security/TestData/CatalogTestData/UserConfigProv +powershell/test/powershell/Modules/Microsoft.PowerShell.Security/TestData/CatalogTestData/UserConfigProv/DSCResources +powershell/test/powershell/Modules/Microsoft.PowerShell.Security/TestData/CatalogTestData/UserConfigProv/DSCResources/scriptdsc +powershell/test/powershell/Modules/Microsoft.PowerShell.Security/TestData/CatalogTestData/UserConfigProv/DSCResources/UserConfigProviderModVersion1 +powershell/test/powershell/Modules/Microsoft.PowerShell.Security/TestData/CatalogTestData/UserConfigProv/DSCResources/UserConfigProviderModVersion2 +powershell/test/powershell/Modules/Microsoft.PowerShell.Security/TestData/CatalogTestData/UserConfigProv/DSCResources/UserConfigProviderModVersion3 +powershell/test/powershell/Modules/Microsoft.PowerShell.Utility/assets +powershell/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/de-DE +powershell/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/en-US +powershell/test/powershell/Modules/PSDesiredStateConfiguration/assets +powershell/test/powershell/Modules/PSDesiredStateConfiguration/assets/dsc +powershell/test/powershell/Modules/PSDesiredStateConfiguration/assets/dsc/baseregistration +powershell/test/powershell/Modules/PSDesiredStateConfiguration/assets/dsc/schema +powershell/test/powershell/Modules/PSDesiredStateConfiguration/assets/dsc/schema/MSFT_LogResource +powershell/test/powershell/Modules/PSDesiredStateConfiguration/assets/dsc/schema/MSFT_nxArchiveResource +powershell/test/powershell/Modules/PSDesiredStateConfiguration/assets/dsc/schema/MSFT_nxEnvironmentResource +powershell/test/powershell/Modules/PSDesiredStateConfiguration/assets/dsc/schema/MSFT_nxFileLineResource +powershell/test/powershell/Modules/PSDesiredStateConfiguration/assets/dsc/schema/MSFT_nxFileResource +powershell/test/powershell/Modules/PSDesiredStateConfiguration/assets/dsc/schema/MSFT_nxGroupResource +powershell/test/powershell/Modules/PSDesiredStateConfiguration/assets/dsc/schema/MSFT_nxPackageResource +powershell/test/powershell/Modules/PSDesiredStateConfiguration/assets/dsc/schema/MSFT_nxScriptResource +powershell/test/powershell/Modules/PSDesiredStateConfiguration/assets/dsc/schema/MSFT_nxServiceResource +powershell/test/powershell/Modules/PSDesiredStateConfiguration/assets/dsc/schema/MSFT_nxSshAuthorizedKeysResource +powershell/test/powershell/Modules/PSDesiredStateConfiguration/assets/dsc/schema/MSFT_nxUserResource +powershell/test/powershell/SDK/assets +powershell/test/tools/CodeCoverageAutomation +powershell/test/tools/Modules +powershell/test/tools/OpenCover +powershell/test/tools/TestExe +powershell/test/tools/WebListener +powershell/test/tools/Modules/HelpersCommon +powershell/test/tools/Modules/HelpersHostCS +powershell/test/tools/Modules/HelpersLanguage +powershell/test/tools/Modules/HelpersRemoting +powershell/test/tools/Modules/HttpListener +powershell/test/tools/Modules/WebListener +powershell/test/tools/WebListener/Controllers +powershell/test/tools/WebListener/Models +powershell/test/tools/WebListener/Views +powershell/test/tools/WebListener/Views/Encoding +powershell/test/tools/WebListener/Views/Home +powershell/test/tools/WebListener/Views/Multipart +powershell/test/tools/WebListener/Views/Redirect +powershell/test/tools/WebListener/Views/Shared +powershell/tools/credScan +powershell/tools/failingTests +powershell/tools/packaging +powershell/tools/releaseBuild +powershell/tools/ResxGen +powershell/tools/terms +powershell/tools/packaging/macos +powershell/tools/packaging/project +powershell/tools/packaging/macos/launcher +powershell/tools/packaging/macos/launcher/ROOT +powershell/tools/packaging/macos/launcher/ROOT/Applications +powershell/tools/packaging/macos/launcher/ROOT/Applications/PowerShell.app +powershell/tools/packaging/macos/launcher/ROOT/Applications/PowerShell.app/Contents +powershell/tools/packaging/macos/launcher/ROOT/Applications/PowerShell.app/Contents/MacOS +powershell/tools/releaseBuild/Images +powershell/tools/releaseBuild/macOS +powershell/tools/releaseBuild/Images/GenericLinuxFiles +powershell/tools/releaseBuild/Images/microsoft_powershell_centos7 +powershell/tools/releaseBuild/Images/microsoft_powershell_ubuntu14.04 +powershell/tools/releaseBuild/Images/microsoft_powershell_ubuntu16.04 +powershell/tools/releaseBuild/Images/microsoft_powershell_windowsservercore +powershell/directory with spaces/child one +powershell/directory with spaces/child two +powershell/directory[with]squarebrackets/one +powershell/directory[with]squarebrackets/two +powershell/powershell +powershell/reallyreallyreallyreallyreallreallyreallyreally_longdirectoryname From bc279b2c3c8dc5e14f13ecd6a57d2c56b64c5b75 Mon Sep 17 00:00:00 2001 From: Nick Date: Mon, 26 Apr 2021 11:02:28 +1000 Subject: [PATCH 03/16] Add recent dirs file locking. --- .gitignore | 1 + cd-extras/private/Core.ps1 | 58 +++++++++++++++++++++++++---------- cd-extras/public/_Classes.ps1 | 3 +- coverage.xml | 1 - 4 files changed, 45 insertions(+), 18 deletions(-) delete mode 100644 coverage.xml diff --git a/.gitignore b/.gitignore index 246babc..64e8011 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ _reports/ ps_modules out/ cd-extras/about_Cd-Extras.help.txt +coverage.xml diff --git a/cd-extras/private/Core.ps1 b/cd-extras/private/Core.ps1 index 794f53b..d1c53f1 100644 --- a/cd-extras/private/Core.ps1 +++ b/cd-extras/private/Core.ps1 @@ -142,9 +142,11 @@ function RegisterCompletions([string[]] $commands, $param, $target) { function ImportRecent() { $dirs = Import-Csv $cde.RECENT_DIRS_FILE - $cde.recentHash = if ($h = Get-FileHash -LiteralPath $cde.RECENT_DIRS_FILE -ErrorAction Ignore) { + $cde.recentHash = if ($h = Get-FileHash -LiteralPath $cde.RECENT_DIRS_FILE) { $h.Hash.ToString() } + + $recent.Clear() $dirs.ForEach{ $dir = [RecentDir]$_ $dir.Favour = $_.Favour -and [bool]::Parse($_.Favour) @@ -153,14 +155,20 @@ function ImportRecent() { } function RefreshRecent() { - # assumes we already know the file exists if (!$cde.RECENT_DIRS_FILE) { return } - $currentHash = (Get-FileHash -LiteralPath $cde.RECENT_DIRS_FILE).Hash.ToString() - if ($currentHash -ne $cde.recentHash) { - WriteLog 'RecentDirs file has changed' - $recent.Clear() - ImportRecent + try { + if ($hasMutex = $cde.mutex.WaitOne(1)) { + # assumes we already know the file exists + $currentHash = (Get-FileHash -LiteralPath $cde.RECENT_DIRS_FILE).Hash.ToString() + if ($currentHash -ne $cde.recentHash) { + WriteLog ($currentHash, $cde.recentHash) + ImportRecent + } + } + } + finally { + if ($hasMutex) { $cde.mutex.ReleaseMutex() } } } @@ -206,7 +214,7 @@ function GetRecent([int] $first, [string[]] $terms) { } function UpdateRecent($path, $favour = $false) { - if ($path -in ($cde.RECENT_DIRS_EXCLUDE | Resolve-Path).Path) { return } + if ($path -in $cde.RECENT_DIRS_EXCLUDE) { return } $entry = if (($current = $recent[$path])) { $current } @@ -225,8 +233,8 @@ function UpdateRecent($path, $favour = $false) { if ($recent.Count -gt $cde.MaxRecentDirs) { RemoveRecent ( $recent.Values | - sort LastEntered | - select -First ($recent.Count - $cde.MaxRecentDirs) -expand Path ) + sort Favour, LastEntered | + select -First ($recent.Count - $cde.MaxRecentDirs) -expand Path) } PersistRecent @@ -248,18 +256,36 @@ function RemoveRecent([string[]] $dirs) { function PersistRecent() { if ($cde.RECENT_DIRS_FILE) { if (!$background) { InitRunspace } - $background.Stop() - $null = $background.BeginInvoke() + + try { + if ($hasMutex = $cde.mutex.WaitOne(1000)) { + $background.Stop() + $null = $background.BeginInvoke() + } + else { + WriteLog 'Recent dirs file in use' + } + } + + finally { + if ($hasMutex) { $cde.mutex.ReleaseMutex() } + } } } function InitRunspace() { # infra for backgrounding recent dirs persistence - $script:background = [PowerShell]::Create() + $Script:background = [PowerShell]::Create() $null = $background.AddScript( { - $recent.Values | Export-Csv -LiteralPath $cde.RECENT_DIRS_FILE - $cde.recentHash = (Get-FileHash $cde.RECENT_DIRS_FILE).Hash.ToString() - } ) + try { + if ($hasMutex = $cde.mutex.WaitOne(1000)) { + $recent.Values | Export-Csv -LiteralPath $cde.RECENT_DIRS_FILE + Write-Verbose ($cde.recentHash = (Get-FileHash $cde.RECENT_DIRS_FILE).Hash.ToString()) + } + } + finally { + if ($hasMutex) { $cde.mutex.ReleaseMutex() } + } }) $runspace = [RunspaceFactory]::CreateRunspace() $runspace.Open() diff --git a/cd-extras/public/_Classes.ps1 b/cd-extras/public/_Classes.ps1 index 34d03cf..230d250 100644 --- a/cd-extras/public/_Classes.ps1 +++ b/cd-extras/public/_Classes.ps1 @@ -17,6 +17,7 @@ class RecentDir { class CdeOptions { hidden [string] $recentHash + hidden [Threading.Mutex] $mutex = [Threading.Mutex]::new($false, 'cde.RECENT_DIRS_FILE') [bool] $AUTO_CD = $true [bool] $CDABLE_VARS = $false @@ -25,7 +26,7 @@ class CdeOptions { [string] $RECENT_DIRS_FILE = $null [string[]] $RECENT_DIRS_EXCLUDE = @() [bool] $RecentDirsFallThrough = $true - [ushort] $MaxRecentDirs = 800 + [ushort] $MaxRecentDirs = 150 [ushort] $MaxRecentCompletions = 100 [ushort] $MaxCompletions = 0 [ushort] $MaxMenuLength = 36 diff --git a/coverage.xml b/coverage.xml deleted file mode 100644 index 3cc6f2b..0000000 --- a/coverage.xml +++ /dev/null @@ -1 +0,0 @@ - From 2bd93434bbce203a8601e4b084944e2bc8f50412 Mon Sep 17 00:00:00 2001 From: Nick Date: Mon, 26 Apr 2021 11:02:37 +1000 Subject: [PATCH 04/16] Add test. --- tests/cd-extras.Tests.ps1 | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/cd-extras.Tests.ps1 b/tests/cd-extras.Tests.ps1 index b0dcf04..91b40f9 100644 --- a/tests/cd-extras.Tests.ps1 +++ b/tests/cd-extras.Tests.ps1 @@ -403,6 +403,23 @@ Describe 'cd-extras' { Get-Bookmark | Remove-Bookmark @(Get-Bookmark).Count | Should -Be 0 } + + It 'adds bookmarks to the recent directories list if not already there' { + cd /powershell/tools/terms + cd / + @(Get-RecentLocation).Count | Should -Be 1 + + '/powershell/tools/failingTests', + '/powershell/tools/packaging', + '/powershell/tools/releaseBuild', + '/powershell/tools/ResxGen', + '/powershell/tools/terms' | mark + + @(Get-RecentLocation).Count | Should -Be 5 + + Remove-Bookmark * + @(Get-RecentLocation).Count | Should -Be 1 + } } Describe 'Multi-dot cd' { From 4c358460cb3a5153b07fcd4faafa4d269dd308a3 Mon Sep 17 00:00:00 2001 From: Nick Date: Mon, 26 Apr 2021 11:31:35 +1000 Subject: [PATCH 05/16] Fix tests and sort on Linux. --- cd-extras/private/Core.ps1 | 6 +- cd-extras/public/Get-Bookmark.ps1 | 4 +- tests/cd-extras.Tests.ps1 | 132 +++++++++++++++--------------- 3 files changed, 71 insertions(+), 71 deletions(-) diff --git a/cd-extras/private/Core.ps1 b/cd-extras/private/Core.ps1 index d1c53f1..bd70711 100644 --- a/cd-extras/private/Core.ps1 +++ b/cd-extras/private/Core.ps1 @@ -176,7 +176,7 @@ function RecentsByTermWithSort([int] $first, [string[]] $terms, [scriptblock] $s function MatchesTerms([string] $path) { function MatchPath() { $indexes = $terms.ForEach{ $path.IndexOf($_, [System.StringComparison]::CurrentCultureIgnoreCase) }.Where{ $_ -gt 0 } - $indexes.Count -eq $terms.Count -and (!(Compare-Object -SyncWindow 0 $indexes ($indexes | sort))) + $indexes.Count -eq $terms.Count -and (!(Compare-Object -SyncWindow 0 $indexes ($indexes | Sort-Object))) } function MatchLeaf() { (Split-Path -Leaf $path) -match $terms[-1] } @@ -186,7 +186,7 @@ function RecentsByTermWithSort([int] $first, [string[]] $terms, [scriptblock] $s RefreshRecent $recent.Values.Where( { ($_.Path -ne $pwd) -and (MatchesTerms $_.Path) }) | - sort $sort -Descending | + Sort-Object $sort -Descending | select -First $first -Expand Path } @@ -233,7 +233,7 @@ function UpdateRecent($path, $favour = $false) { if ($recent.Count -gt $cde.MaxRecentDirs) { RemoveRecent ( $recent.Values | - sort Favour, LastEntered | + Sort-Object Favour, LastEntered | select -First ($recent.Count - $cde.MaxRecentDirs) -expand Path) } diff --git a/cd-extras/public/Get-Bookmark.ps1 b/cd-extras/public/Get-Bookmark.ps1 index 6594112..7ff85ce 100644 --- a/cd-extras/public/Get-Bookmark.ps1 +++ b/cd-extras/public/Get-Bookmark.ps1 @@ -24,12 +24,12 @@ Set-FrecentLocation #> function Get-Bookmark { - + [OutputType([string[]])] param( [Parameter(Position = 0)] [ushort] $First = $cde.MaxRecentCompletions ) $recent.Values.Where{ $_.Favour } | - sort EnterCount, LastEntered -Descending | + Sort-Object EnterCount, LastEntered -Descending | select -First $First -Expand Path } diff --git a/tests/cd-extras.Tests.ps1 b/tests/cd-extras.Tests.ps1 index 91b40f9..3df7289 100644 --- a/tests/cd-extras.Tests.ps1 +++ b/tests/cd-extras.Tests.ps1 @@ -209,8 +209,8 @@ Describe 'cd-extras' { BeforeEach { Remove-RecentLocation * } It 'toggles between two directories' { - cd /powershell/tools/ResxGen - cd /powershell/tools/terms + cd TestDrive:/powershell/tools/ResxGen + cd TestDrive:/powershell/tools/terms cdr CurrentDir | Should -Be ResxGen cdr @@ -218,8 +218,8 @@ Describe 'cd-extras' { } It 'supports the PassThru switch' { - cd /powershell/tools/ResxGen - cd /powershell/tools/terms + cd TestDrive:/powershell/tools/ResxGen + cd TestDrive:/powershell/tools/terms $path = cdr -PassThru $path | SPlit-Path -Leaf | Should -Be ResxGen $path = cdr -PassThru @@ -227,24 +227,24 @@ Describe 'cd-extras' { } It 'can move to a recent location by name' { - cd /powershell/tools/ResxGen - cd /powershell/tools/terms - cd / + cd TestDrive:/powershell/tools/ResxGen + cd TestDrive:/powershell/tools/terms + cd TestDrive:/ $thru = cdr tool, resx -PassThru Get-Location | Split-Path -Leaf | Should -be 'ResxGen' $thru.Path | Should -BeLike '*ResxGen' } It 'truncates the recent locations list if necessary' { - cd /powershell/tools/failingTests - cd /powershell/tools/packaging - cd /powershell/tools/releaseBuild - cd /powershell/tools/ResxGen - cd /powershell/tools/terms - cd / + cd TestDrive:/powershell/tools/failingTests + cd TestDrive:/powershell/tools/packaging + cd TestDrive:/powershell/tools/releaseBuild + cd TestDrive:/powershell/tools/ResxGen + cd TestDrive:/powershell/tools/terms + cd TestDrive:/ (cdr -l).Count | Should -Be 5 setocd MaxRecentDirs 3 - cd /powershell + cd TestDrive:/powershell (cdr -l).Count | Should -Be 2 setocd MaxRecentDirs 10 } @@ -261,10 +261,10 @@ Describe 'cd-extras' { $Error.Clear() } - It 'can list andprune' { - cd /powershell/tools/ResxGen - cd /powershell/tools/terms - cd / + It 'can list and prune' { + cd TestDrive:/powershell/tools/ResxGen + cd TestDrive:/powershell/tools/terms + cd TestDrive:/ (cdr -l).Count | Should -Be 2 cdr -p * @@ -276,16 +276,16 @@ Describe 'cd-extras' { BeforeEach { Remove-RecentLocation * } It 'returns most recent directories in order' { - cd /powershell/tools/ResxGen - cd /powershell/tools/terms - cd / + cd TestDrive:/powershell/tools/ResxGen + cd TestDrive:/powershell/tools/terms + cd TestDrive:/ Get-RecentLocation | select -Expand Name | Should -Be 'terms', 'ResxGen' } It 'refreshes the list if RECENT_DIRS_FILE updated in another process' { Get-RecentLocation | Should -BeNullOrEmpty - $fst, $snd = ((Resolve-Path /), (Resolve-Path /powershell)).Path + $fst, $snd = ((Resolve-Path TestDrive:/), (Resolve-Path TestDrive:/powershell)).Path $newList = [Collections.Generic.Dictionary[string, RecentDir]]::new() $newList[$fst] = @{ Path = $fst @@ -311,9 +311,9 @@ Describe 'cd-extras' { It 'can move to the nth most frecent location (with passthrough)' { cdf -l | Should -BeNullOrEmpty - cd /powershell/tools/ResxGen - cd /powershell/tools/terms - cd / + cd TestDrive:/powershell/tools/ResxGen + cd TestDrive:/powershell/tools/terms + cd TestDrive:/ $frecents = cdf -l $thru = cdf 2 -PassThru Get-Location | Split-Path -Leaf | Should -be $frecents[1].Name @@ -321,9 +321,9 @@ Describe 'cd-extras' { } It 'can move to a frecent location by name' { - cd /powershell/tools/ResxGen - cd /powershell/tools/terms - cd / + cd TestDrive:/powershell/tools/ResxGen + cd TestDrive:/powershell/tools/terms + cd TestDrive:/ $thru = cdf term -PassThru Get-Location | Split-Path -Leaf | Should -be 'terms' $thru.Path | Should -BeLike '*terms' @@ -342,9 +342,9 @@ Describe 'cd-extras' { } It 'can list, prune and mark' { - cd /powershell/tools/ResxGen - cd /powershell/tools/terms - cd / + cd TestDrive:/powershell/tools/ResxGen + cd TestDrive:/powershell/tools/terms + cd TestDrive:/ (cdf -l).Count | Should -Be 2 cdf -p * @@ -363,11 +363,11 @@ Describe 'cd-extras' { It 'prefers highest ranked directory when last accessed times are similar' { Get-FrecentLocation | Should -BeNullOrEmpty - cd /powershell/tools/terms - cd /powershell/tools/ResxGen - cd / - cd /powershell/tools/ResxGen - cd / + cd TestDrive:/powershell/tools/terms + cd TestDrive:/powershell/tools/ResxGen + cd TestDrive:/ + cd TestDrive:/powershell/tools/ResxGen + cd TestDrive:/ $actual = Get-FrecentLocation | select -Expand Name $actual[0] | Should -Be 'resxgen' $actual[1] | Should -Be 'terms' @@ -375,11 +375,11 @@ Describe 'cd-extras' { It 'prefers bookmarked directory regardless of frecency' { Get-FrecentLocation | Should -BeNullOrEmpty - cd /powershell/tools/terms - cd /powershell/tools/ResxGen - cd / - cd /powershell/tools/ResxGen - cd / + cd TestDrive:/powershell/tools/terms + cd TestDrive:/powershell/tools/ResxGen + cd TestDrive:/ + cd TestDrive:/powershell/tools/ResxGen + cd TestDrive:/ mark powershell/tools/packaging $actual = Get-FrecentLocation | select -Expand Name $actual[0] | Should -Be 'packaging' @@ -392,11 +392,11 @@ Describe 'cd-extras' { BeforeEach { Remove-RecentLocation * } It 'can bookmark multiple directories' { - '/powershell/tools/failingTests', - '/powershell/tools/packaging', - '/powershell/tools/releaseBuild', - '/powershell/tools/ResxGen', - '/powershell/tools/terms' | mark + 'TestDrive:/powershell/tools/failingTests', + 'TestDrive:/powershell/tools/packaging', + 'TestDrive:/powershell/tools/releaseBuild', + 'TestDrive:/powershell/tools/ResxGen', + 'TestDrive:/powershell/tools/terms' | mark (Get-Bookmark).Count | Should -Be 5 @@ -405,15 +405,15 @@ Describe 'cd-extras' { } It 'adds bookmarks to the recent directories list if not already there' { - cd /powershell/tools/terms - cd / + cd TestDrive:/powershell/tools/terms + cd TestDrive:/ @(Get-RecentLocation).Count | Should -Be 1 - '/powershell/tools/failingTests', - '/powershell/tools/packaging', - '/powershell/tools/releaseBuild', - '/powershell/tools/ResxGen', - '/powershell/tools/terms' | mark + 'TestDrive:/powershell/tools/failingTests', + 'TestDrive:/powershell/tools/packaging', + 'TestDrive:/powershell/tools/releaseBuild', + 'TestDrive:/powershell/tools/ResxGen', + 'TestDrive:/powershell/tools/terms' | mark @(Get-RecentLocation).Count | Should -Be 5 @@ -1191,9 +1191,9 @@ Describe 'cd-extras' { BeforeEach { Remove-RecentLocation * } It 'expands recent directories in order' { - cd /powershell/tools/terms - cd /powershell/tools/ResxGen - cd / + cd TestDrive:/powershell/tools/terms + cd TestDrive:/powershell/tools/ResxGen + cd TestDrive:/ $actual = CompleteRecent -wordToComplete '' $actual[0].CompletionText | Should -BeLike "*resxgen" $actual[1].CompletionText | Should -BeLike "*terms" @@ -1240,24 +1240,24 @@ Describe 'cd-extras' { It 'Persists, loads or creates recent dirs file as necessary' { Remove-RecentLocation * - cd /powershell/tools/terms - cd /powershell/tools/ResxGen - cd / + cd TestDrive:/powershell/tools/terms + cd TestDrive:/powershell/tools/ResxGen + cd TestDrive:/ - setocd RECENT_DIRS_FILE /recent_dirs - Sleep -Milliseconds 10 # save is async - (Get-Content ./recent_dirs).Length | Should -Be 4 # including header + setocd RECENT_DIRS_FILE TestDrive:/recent_dirs + Start-Sleep -Milliseconds 10 # save is async + (Get-Content TestDrive:/recent_dirs).Length | Should -Be 4 # including header setocd RECENT_DIRS_FILE Remove-RecentLocation * Get-RecentLocation | Should -BeNullOrEmpty - setocd RECENT_DIRS_FILE /recent_dirs + setocd RECENT_DIRS_FILE TestDrive:/recent_dirs (Get-RecentLocation).Count | Should -Be 2 Remove-RecentLocation * - Test-Path /recent_dirs_2 | Should -Be $false - setocd RECENT_DIRS_FILE /recent_dirs_2 - Test-Path /recent_dirs_2 | Should -Be $true + Test-Path TestDrive:/recent_dirs_2 | Should -Be $false + setocd RECENT_DIRS_FILE TestDrive:/recent_dirs_2 + Test-Path TestDrive:/recent_dirs_2 | Should -Be $true setocd RECENT_DIRS_FILE } From 4e0c88dc29d873ba29d4ca9d77b1da86066d0d9d Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 10 May 2022 11:08:39 +1000 Subject: [PATCH 06/16] Remove path completion warning beep. --- cd-extras/cd-extras.psm1 | 5 +- cd-extras/private/CompletePaths.ps1 | 2 +- cd-extras/private/Core.ps1 | 27 +-- cd-extras/public/Add-Bookmark.ps1 | 2 +- cd-extras/public/Expand-Path.ps1 | 4 +- cd-extras/public/Get-FrecentLocation.ps1 | 1 - cd-extras/public/Get-Up.ps1 | 2 +- cd-extras/public/Redo-Location.ps1 | 2 +- cd-extras/public/Set-CdExtrasOption.ps1 | 5 +- cd-extras/public/Set-FrecentLocation.ps1 | 7 +- cd-extras/public/Set-RecentLocation.ps1 | 13 +- cd-extras/public/Undo-Location.ps1 | 4 +- cd-extras/public/_Classes.ps1 | 4 +- readme.md | 231 ++++++++++++++++------- 14 files changed, 207 insertions(+), 102 deletions(-) diff --git a/cd-extras/cd-extras.psm1 b/cd-extras/cd-extras.psm1 index 6d66ece..1c12dc4 100644 --- a/cd-extras/cd-extras.psm1 +++ b/cd-extras/cd-extras.psm1 @@ -1,7 +1,8 @@ $cdAlias = if ($x = (Get-Alias -Name 'cd' -ErrorAction ignore)) { $x.Definition } -Get-ChildItem $PSScriptRoot/private/*.ps1 | % { . $_.FullName } -Get-ChildItem $PSScriptRoot/public/*.ps1 | % { . $_.FullName } +Get-ChildItem -File -Filter *.ps1 $PSScriptRoot/private, $PSScriptRoot/public | % { + . $_.FullName +} # remove stupid phantom module Get-Module | Where Path -eq ("$PSScriptRoot/public/_Classes.ps1" | Resolve-Path) | Remove-Module diff --git a/cd-extras/private/CompletePaths.ps1 b/cd-extras/private/CompletePaths.ps1 index 20cba4a..46b00fe 100644 --- a/cd-extras/private/CompletePaths.ps1 +++ b/cd-extras/private/CompletePaths.ps1 @@ -62,7 +62,7 @@ function CompletePaths { if ($n -le 1) { $_ } else { "$_ ($n)" } } - $tooltip = if ($cde.ToolTip) { &$cde.ToolTip $_ $isListTruncated } else { $_ } + $tooltip = if ($cde.ToolTip -and ($tooltip = &$cde.ToolTip $_ $isListTruncated)) { $tooltip } else { $_ } [Management.Automation.CompletionResult]::new( $completionText, diff --git a/cd-extras/private/Core.ps1 b/cd-extras/private/Core.ps1 index bd70711..5345de3 100644 --- a/cd-extras/private/Core.ps1 +++ b/cd-extras/private/Core.ps1 @@ -82,18 +82,18 @@ filter EscapeWildcards { [WildcardPattern]::Escape($_) } -function GetStackIndex([array]$stack, [string]$namepart) { +function GetBestIndex([array]$array, [string]$namepart) { ( - $items = $stack -eq ($namepart | Normalise | RemoveTrailingSeparator) # full path match + $items = $array -eq ($namepart | Normalise | RemoveTrailingSeparator) # full path match ) -or ( - $items = $stack.Where{ ($_ | Split-Path -Leaf) -eq $namepart } # full leaf match + $items = $array.Where{ ($_ | Split-Path -Leaf) -eq $namepart } # full leaf match ) -or ( - $items = $stack.Where{ ($_ | Split-Path -Leaf) -Match "^$($namepart | NormaliseAndEscape)" } # leaf starts with + $items = $array.Where{ ($_ | Split-Path -Leaf) -Match "^$($namepart | NormaliseAndEscape)" } # leaf starts with ) -or ( - $items = $stack -match ($namepart | NormaliseAndEscape) # anything... + $items = $array -match ($namepart | NormaliseAndEscape) # anything... ) | Out-Null - [array]::indexOf($stack, ($items | select -First 1)) + [array]::indexOf($array, ($items | select -First 1)) } function IndexedComplete([bool] $IndexedCompletion = $cde.IndexedCompletion) { @@ -125,7 +125,7 @@ function IndexPaths( [array]$xs, $rootLabel = 'root' # this on happens on *nix ) { - $xs = $xs -ne '' + $xs = $xs -ne '' | Select -Unique if (!$xs) { return } $i = 0 @@ -174,14 +174,16 @@ function RefreshRecent() { function RecentsByTermWithSort([int] $first, [string[]] $terms, [scriptblock] $sort) { function MatchesTerms([string] $path) { - function MatchPath() { - $indexes = $terms.ForEach{ $path.IndexOf($_, [System.StringComparison]::CurrentCultureIgnoreCase) }.Where{ $_ -gt 0 } - $indexes.Count -eq $terms.Count -and (!(Compare-Object -SyncWindow 0 $indexes ($indexes | Sort-Object))) + function MatchPath($terms, $idx = 0) { + $fst, $rst = $terms + if (!$fst) { return $true } + $nextIdx = $path.IndexOf($fst, $idx, [StringComparison]::CurrentCultureIgnoreCase) + return ($nextIdx -ge 0) -and (MatchPath $rst ($nextIdx + $fst.Length)) } - function MatchLeaf() { (Split-Path -Leaf $path) -match $terms[-1] } + function MatchLeaf($term) { (Split-Path -Leaf $path) -match $term } if (!$terms) { return $true } - (MatchPath) -and (MatchLeaf) + (MatchPath ($terms | Normalise)) -and (MatchLeaf ($terms[-1] | NormaliseAndEscape)) } RefreshRecent @@ -214,6 +216,7 @@ function GetRecent([int] $first, [string[]] $terms) { } function UpdateRecent($path, $favour = $false) { + $path = $path | RemoveTrailingSeparator if ($path -in $cde.RECENT_DIRS_EXCLUDE) { return } $entry = diff --git a/cd-extras/public/Add-Bookmark.ps1 b/cd-extras/public/Add-Bookmark.ps1 index b7917be..c3b276e 100644 --- a/cd-extras/public/Add-Bookmark.ps1 +++ b/cd-extras/public/Add-Bookmark.ps1 @@ -31,5 +31,5 @@ function Add-Bookmark() { [Parameter(Position = 0, ValueFromPipeline)] [string] $Path = $PWD ) - Process { UpdateRecent (Resolve-Path $Path) $true } + Process { if (Test-Path $Path) { UpdateRecent (Resolve-Path $Path) $true } } } diff --git a/cd-extras/public/Expand-Path.ps1 b/cd-extras/public/Expand-Path.ps1 index 931075c..961a366 100644 --- a/cd-extras/public/Expand-Path.ps1 +++ b/cd-extras/public/Expand-Path.ps1 @@ -66,12 +66,14 @@ function Expand-Path { [switch] $Force ) - Process { + Begin { $delimiterGroup = if ($WordDelimiters) { '[{0}]' -f [Regex]::Escape($WordDelimiters -join '') } else { '$^' } # no delimiters + } + Process { # replace multi-dot with an appropriate number of `../` $multiDot = [regex]::Match($Path, '^\.{3,}').Value $replacement = ('../' * [Math]::Max(0, $multiDot.Length - 1)) -replace '.$' diff --git a/cd-extras/public/Get-FrecentLocation.ps1 b/cd-extras/public/Get-FrecentLocation.ps1 index 3030728..c203d16 100644 --- a/cd-extras/public/Get-FrecentLocation.ps1 +++ b/cd-extras/public/Get-FrecentLocation.ps1 @@ -51,7 +51,6 @@ Remove-RecentLocation function Get-FrecentLocation { [OutputType([IndexedPath])] - [CmdletBinding(DefaultParameterSetName = '')] param( [Parameter(ParameterSetName = 'First')] [ushort] $First = $cde.MaxRecentCompletions, [Parameter(ValueFromRemainingArguments)] [string[]] $Terms diff --git a/cd-extras/public/Get-Up.ps1 b/cd-extras/public/Get-Up.ps1 index 4402142..91998b5 100644 --- a/cd-extras/public/Get-Up.ps1 +++ b/cd-extras/public/Get-Up.ps1 @@ -70,7 +70,7 @@ function Get-Up { if ($PSCmdlet.ParameterSetName -eq 'named') { - if (($match = GetStackIndex $ancestors $NamePart) -ge 0) { + if (($match = GetBestIndex $ancestors $NamePart) -ge 0) { $ancestors[$match] } else { diff --git a/cd-extras/public/Redo-Location.ps1 b/cd-extras/public/Redo-Location.ps1 index 250a858..99383c3 100644 --- a/cd-extras/public/Redo-Location.ps1 +++ b/cd-extras/public/Redo-Location.ps1 @@ -50,7 +50,7 @@ function Redo-Location { if ($PSCmdlet.ParameterSetName -eq 'named') { - if (($match = GetStackIndex $redoStack.ToArray() $NamePart) -ge 0) { + if (($match = GetBestIndex $redoStack.ToArray() $NamePart) -ge 0) { Redo-Location ($match + 1) } else { diff --git a/cd-extras/public/Set-CdExtrasOption.ps1 b/cd-extras/public/Set-CdExtrasOption.ps1 index 6cd53be..a422838 100644 --- a/cd-extras/public/Set-CdExtrasOption.ps1 +++ b/cd-extras/public/Set-CdExtrasOption.ps1 @@ -35,7 +35,7 @@ function Set-CdExtrasOption { [Parameter(ParameterSetName = 'Set', Position = 1, ValueFromPipeline)] $Value, - [Parameter(ParameterSetName = 'Validate', Mandatory, ValueFromPipeline)] + [Parameter(ParameterSetName = 'Validate', Mandatory)] [switch] $Validate ) @@ -73,7 +73,7 @@ function Set-CdExtrasOption { $path = $cde.RECENT_DIRS_FILE -replace '~', $HOME $cde.RECENT_DIRS_FILE = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath( $path ) - # save recent dirs from memory when dirs file first set + # save recent dirs from memory when dirs file set if ($recent.Count) { PersistRecent } # load recent dirs into memory at startup @@ -107,5 +107,6 @@ function Set-CdExtrasOption { CommandNotFound @() $isUnderTest } + # can be used to ensure side effects have run without actually changing any options if ($Validate) { return $true } } diff --git a/cd-extras/public/Set-FrecentLocation.ps1 b/cd-extras/public/Set-FrecentLocation.ps1 index 82848ea..714e6f4 100644 --- a/cd-extras/public/Set-FrecentLocation.ps1 +++ b/cd-extras/public/Set-FrecentLocation.ps1 @@ -72,6 +72,7 @@ function Set-FrecentLocation { [Parameter(ParameterSetName = 'n', Position = 0)] [ushort] $n = 1, + [Alias('NamePart')] [Parameter(ParameterSetName = 'named', Position = 0)] [string[]] $Terms, @@ -86,20 +87,20 @@ function Set-FrecentLocation { [Alias('p')] [Parameter(ParameterSetName = 'prune', Mandatory)] [switch] $Prune, - [Parameter(ParameterSetName = 'prune', Position = 1, ValueFromPipeline)] + [Parameter(ParameterSetName = 'prune', Position = 1)] [SupportsWildcards()] [string] $PrunePattern = $PWD, [Alias('m')] [Parameter(ParameterSetName = 'mark', Mandatory)] [switch] $Mark, - [Parameter(ParameterSetName = 'mark', Position = 1, ValueFromPipeline)] + [Parameter(ParameterSetName = 'mark', Position = 1)] [string] $MarkPath = $PWD, [Alias('u')] [Parameter(ParameterSetName = 'unmark', Mandatory)] [switch] $Unmark, - [Parameter(ParameterSetName = 'unmark', Position = 1, ValueFromPipeline)] + [Parameter(ParameterSetName = 'unmark', Position = 1)] [string] $UnmarkPattern = $PWD, [switch] $PassThru diff --git a/cd-extras/public/Set-RecentLocation.ps1 b/cd-extras/public/Set-RecentLocation.ps1 index 56a5c86..70343c0 100644 --- a/cd-extras/public/Set-RecentLocation.ps1 +++ b/cd-extras/public/Set-RecentLocation.ps1 @@ -56,8 +56,9 @@ function Set-RecentLocation { [Parameter(ParameterSetName = 'n', Position = 0)] [ushort] $n = 1, + [Alias('NamePart')] [Parameter(ParameterSetName = 'named', Position = 0)] - [string[]] $NamePart, + [string[]] $Terms, [Alias('l')] [Parameter(ParameterSetName = 'list', Mandatory)] @@ -70,7 +71,7 @@ function Set-RecentLocation { [Alias('p')] [Parameter(ParameterSetName = 'prune', Mandatory)] [switch] $Prune, - [Parameter(ParameterSetName = 'prune', Position = 1, Mandatory, ValueFromPipeline)] + [Parameter(ParameterSetName = 'prune', Position = 1, Mandatory)] [SupportsWildcards()] [string] $PrunePattern, @@ -78,15 +79,15 @@ function Set-RecentLocation { ) if ($PSCmdlet.ParameterSetName -eq 'n' -and $n -ge 1) { - $recents = @(GetRecent $n $NamePart) + $recents = @(GetRecent $n) if ($recents.Count -ge $n) { Set-LocationEx $recents[$n - 1] -PassThru:$PassThru } } if ($PSCmdlet.ParameterSetName -eq 'named') { - $recents = @(GetRecent 1 $NamePart) + $recents = @(GetRecent 1 $Terms) if ($recents) { Set-LocationEx $recents[0] -PassThru:$PassThru } - elseif ($cde.RecentDirsFallThrough -and $NamePart.Length -eq 1) { Set-LocationEx $NamePart[0] -PassThru:$PassThru } - else { Write-Error "Could not find '$NamePart' in recent locations." -ErrorAction Stop } + elseif ($cde.RecentDirsFallThrough -and $Terms.Length -eq 1) { Set-LocationEx $Terms[0] -PassThru:$PassThru } + else { Write-Error "Could not find '$Terms' in recent locations." -ErrorAction Stop } } if ($PSCmdlet.ParameterSetName -eq 'list' -and $List) { diff --git a/cd-extras/public/Undo-Location.ps1 b/cd-extras/public/Undo-Location.ps1 index 801bfac..1a3fa89 100644 --- a/cd-extras/public/Undo-Location.ps1 +++ b/cd-extras/public/Undo-Location.ps1 @@ -31,7 +31,7 @@ PS C:\Windows\System32> _ .LINK Redo-Location -Get-Stack +Get-Stack #> function Undo-Location { [OutputType([void], [Management.Automation.PathInfo])] @@ -57,7 +57,7 @@ function Undo-Location { if ($PSCmdlet.ParameterSetName -eq 'named') { - if (($match = GetStackIndex $undoStack.ToArray() $NamePart) -ge 0) { + if (($match = GetBestIndex $undoStack.ToArray() $NamePart) -ge 0) { Undo-Location ($match + 1) } else { diff --git a/cd-extras/public/_Classes.ps1 b/cd-extras/public/_Classes.ps1 index 230d250..24a2902 100644 --- a/cd-extras/public/_Classes.ps1 +++ b/cd-extras/public/_Classes.ps1 @@ -26,8 +26,8 @@ class CdeOptions { [string] $RECENT_DIRS_FILE = $null [string[]] $RECENT_DIRS_EXCLUDE = @() [bool] $RecentDirsFallThrough = $true - [ushort] $MaxRecentDirs = 150 - [ushort] $MaxRecentCompletions = 100 + [ushort] $MaxRecentDirs = 120 + [ushort] $MaxRecentCompletions = 60 [ushort] $MaxCompletions = 0 [ushort] $MaxMenuLength = 36 [char[]] $WordDelimiters = '.', '_', '-' diff --git a/readme.md b/readme.md index e736873..80120f3 100644 --- a/readme.md +++ b/readme.md @@ -11,8 +11,11 @@ cd-extras - [cd-extras](#cd-extras) -- [Navigation helper commands](#navigation-helper-commands) +- [Navigation commands](#navigation-commands) - [Parameters](#parameters) + - [Frecency](#frecency) + - [Database](#database) + - [Bookmarks](#bookmarks) - [Output](#output) - [Completions](#completions) - [Listing available navigation targets](#listing-available-navigation-targets) @@ -32,7 +35,7 @@ cd-extras - [Multi-dot](#multi-dot) - [CD PATH](#cd-path) - [CDABLE VARS](#cdable-vars) -- [Additional helpers](#additional-helpers) +- [Related commands](#related-commands) - [Get-Up gup](#get-up-gup) - [Get-Stack dirs](#get-stack-dirs) - [Clear-Stack dirsc](#clear-stack-dirsc) @@ -49,9 +52,9 @@ cd-extras -# Navigation helper commands +# Navigation commands -**Quickly navigate backwards, forwards, upwards or into recently used directories* +*Quickly navigate backwards, forwards, upwards or into recently used directories*
[Watch]

@@ -60,27 +63,32 @@ cd-extras
-_cd-extras_ provides the following navigation helpers and corresponding aliases (shown in parens): +_cd-extras_ provides the following navigation commands and corresponding aliases (shown in parens): - `Undo-Location`, (`cd-` or `~`) - `Redo-Location`, (`cd+` or `~~`) -- `Set-RecentLocation`, (`cdr`) - `Step-Up`, (`up`or `..`) +- `Set-RecentLocation`, (`cdr`) +- `Set-FrecentLocation`, (`cdf`) ```powershell [C:/Windows/System32]> up # or .. [C:/Windows]> cd- # or ~ [C:/Windows/System32]> cd+ # or ~~ -[C:/Windows]> █ +[C:/Windows]> cdr +[C:/Windows/System32]> cdr +[C:/Windows]> ▁ ``` :point_right: -That's `cd-` and `cd+`, without a space. `cd -` and `cd +` (with a space) also work but you won't -get [auto-completions](#completions). +That's `cd-` and `cd+`, without a space. `cd -` and `cd +`, with a space, also work but you won't +get [auto-completion](#completions). +:point_right: Repeated uses of `cd-` keep moving backwards towards the beginning of the stack rather than -toggling between the two most recent directories as in vanilla bash. Use `Set-RecentLocation` (`cdr`) -if you want to toggle between directories. +toggling between the two most recent directories as in vanilla bash, neither will it echo the path +of the target directory. Use `Set-RecentLocation` (`cdr`) to toggle between directories and the +`-PassThru` switch if you need to output the new directory path. ```powershell [C:/Windows/System32]> cd .. @@ -90,27 +98,38 @@ if you want to toggle between directories. [C:/Windows/System32]> cd+ [C:/Windows]> cd+ [C:/]> cdr -[C:/Windows]> cdr -[C:/]> █ +[C:/Windows]> cdr -PassThru +Path +---- +C:\ + +[C:/]> ▁ ``` ## Parameters -`up`, `cd+`, `cd-` and `cdr` each take a single optional argument: either a number of steps, `n`... +`up`, `cd+`, `cd-`, `cdr` and `cdf` each take an optional argument, `n` which navigates by the given +number of steps. ```powershell [C:/Windows/System32]> .. 2 # or `up 2` [C:/]> cd temp [C:/temp]> cd- 2 # `cd -2`, `~ 2` or just `~2` also work [C:/Windows/System32]> cd+ 2 -[C:/temp]> █ +[C:/temp]> cdr 2 # cdr ignores the current directory +[C:/]> ▁ ``` -...or a string, `NamePart`, used to select the nearest matching directory from the available -locations. Given a `NamePart`, _cd-extras_ will search from the current location for directories -whose leaf name contains the given string⁽¹⁾. If none is found then it will attempt to find a match -within the full path of each candidate directory⁽²⁾. +`up`, `cd+` and `cd-` also accept a string, `NamePart`, used to select the nearest matching directory +from the available locations. Given a `NamePart`, _cd-extras_ will match the first directory whose +leaf name contains the given string⁽¹⁾. If none is found then it will attempt to match against the +full path of each candidate directory⁽²⁾. + +`cdr` and `cdf` use a slightly different name matching logic which is cribbed from [Zoxide][3]. +Each command takes one or more `Terms` where each term must match part of a target directory, in order, +and the _last_ (or only) term must match the target's leaf name. + ```powershell [C:/Windows]> cd system32 @@ -118,16 +137,82 @@ within the full path of each candidate directory⁽²⁾. [C:/Windows/System32/drivers]> cd- win # [ex. 1] by leaf name [C:/Windows/]> cd+ 32/dr # [ex. 2] by full name [C:/Windows/System32/drivers]> up win # by leaf name -[C:/Windows]> cdr drivers # by leaf again -[C:/Windows/System32/drivers]> █ +[C:/Windows]> cdr drivers # by leaf +[C:/Windows/System32/drivers]> cdr +[C:/Windows]> cdr sys,dr # in sequence +[C:/Windows/System32/drivers]> ▁ +``` + + +## Frecency + +While `Get-RecentLocation` (`cdr -l`) and `Set-RecentLocation` (`cdr`) work with the recent +directories list in the order that they were most recently visited, `Get-FrecentLocation` (`cdf -l`) +and `Set-FrecentLocation` (`cdf`) use a frecency algorithm as described in the [Zoxide docs][4]. + + +## Database + +Recent locations, frecent locations, and bookmarks share a datastore which is not persisted between +sessions by default. +You can opt in to persisting to a CSV file by setting the `RECENT_DIRS_FILE` [option](#configure). + +```powershell +setocd RECENT_DIRS_FILE $env:APPDATA/.recent-dirs +``` + +The size of the datastore - whether persisted or not - is configured with the `MaxRecentDirs` option. +Once the limit is reached, the least recently entered directories are discarded after every directory +change although bookmarked directories are never dropped. + +You can manually remove entries with the `Remove-RecentLocation` command or by using the `-Prune` +switch with `Set-RecentLocation` and `Set-FrecentLocation`. This command expects a parameter, +`Pattern`, which is a PowerShell wildcard pattern used to match against the directory path or a +complete directory leaf name. If no pattern is given then the current working directory is removed. + +```powershell +[~]> Set-Alias z Set-FrecentLocation +[~]> z -l # z -List + +n Name Path +- ---- ---- +1 abc C:\Temp\abc1 +2 abc2 C:\Temp\abc2 +3 def C:\Temp\def + +[~]> z -prune *abc* +[~]> z -l + +n Name Path +- ---- ---- +1 def C:\Temp\def + +[~]> z -p def +[~]> z -l +[~]> ▁ +``` + + +## Bookmarks + +Directories may be bookmarked with the `Add-Bookmark` command (`mark`), boosting those directories +to the top of the frecency list. `Add-Bookmark` takes a `Path` parameter which can be omitted when +bookmarking the current directory. `Set-RecentLocation` (`cdr`) and `Set-FrecentLocation` (`cdf`) +provide a `-Mark` or `-m` switch with the same functionality. + +``` +[~]> Set-Alias z Set-FrecentLocation +[~]> z -mark C:\Temp\abc1 +[~]> z a +[C:\Temp\abc1]> ▁ ``` ## Output -Each helper includes a `-PassThru` switch to return a `PathInfo` value in case you need a -reference to the resulting directory. The value will be `$null` if the action wasn't completed. -(For example, if there was nothing in the stack or you attempted to navigate up from the root.) +Each navigation command includes a `-PassThru` switch to return a `PathInfo` value in case you need +a reference to the resulting directory. The value will be `$null` if the action wasn't completed - +for example, if there was nothing in the stack or you attempted to navigate up from the root. ```powershell [C:/Windows/System32]> up -PassThru @@ -142,13 +227,13 @@ Path ---- C:\Windows\System32 -[C:/Windows/System32]> █ +[C:/Windows/System32]> ▁ ``` ## Completions -Auto-completions are provided for each of `cd-`, `cd+`, and `up`. +Auto-completions are provided for each of `cd-`, `cd+`, `cdr`, `cdf` and `up`. Assuming the [_PSReadLine_][0] `MenuComplete` function is bound to tab... @@ -198,13 +283,13 @@ C:\Windows\System32 ``` -It's also possible to tab-complete `cd-`, `cd+` and `up` using a partial directory name (i.e. the -[`NamePart` parameter](#parameters)). +It's also possible to tab-complete `cd-`, `cd+`, `cdr`, `cdf` and `up` using a partial directory name +(i.e. the [`NamePart` parameter](#parameters)). ```powershell [~/projects/PowerShell/src/Modules/Shared]> up pr⇥ [~/projects/PowerShell/src/Modules/Shared]> up '~\projects' -[~/projects]> █ +[~/projects]> ▁ ``` @@ -214,6 +299,8 @@ As an alternative to menu completion you retrieve a list of available targets wi - `Get-Stack -Undo` (`dirs -u`) - `Get-Stack -Redo` (`dirs -r`) +- `Get-RecentLocations` (`cdr -l`) +- `Get-FrecentLocations` (`cdf -l`) - `Get-Ancestors` (`xup`) ```powershell @@ -235,7 +322,14 @@ n Name Path 2 drivers C:\Windows\System32\drivers [C:/]> cd- 2 -[C:/Windows/System32/drivers]> █ +[C:/Windows/System32/drivers]> cdr -l -First 3 + +n Name Path +- ---- ---- +1 C:\ C:\ +2 Windows C:\Windows +3 drivers C:\Windows\System32\drivers + ``` @@ -272,7 +366,7 @@ If you're not sure whether an unambiguous match is available then just hit tab t [~/projects]> cd cd-e [~/projects/cd-extras]> cd ~ [~]> cd pr/cd -[~/projects/cd-extras]> █ +[~/projects/cd-extras]> ▁ ``` Word delimiters (`.`, `_`, `-` by [default](#configure)) are expanded around so a segment @@ -280,7 +374,7 @@ containing `.sdk` is expanded into `*.sdk*`. ```powershell [~]> cd proj/pow/s/.sdk -[~/projects/powershell/src/Microsoft.PowerShell.SDK]> █ +[~/projects/powershell/src/Microsoft.PowerShell.SDK]> ▁ ``` :point_right: @@ -289,7 +383,7 @@ this... ```powershell [~/projects/powershell]> cd src/-unix -[~/projects/PowerShell/src/powershell-unix]> █ +[~/projects/PowerShell/src/powershell-unix]> ▁ ``` ... you need to escape this: @@ -299,7 +393,7 @@ this... Set-LocationEx: A parameter cannot be found that matches parameter name 'unix'. [~/projects/powershell/src]> cd `-unix # backtick escapes the hyphen -[~/projects/PowerShell/src/powershell-unix]> █ +[~/projects/PowerShell/src/powershell-unix]> ▁ ``` Pairs of periods are expanded between so, for example, a segment containing `s..32` is expanded @@ -307,7 +401,7 @@ into `s*32`. ```powershell [~]> cd /w/s..32/d/et -[C:/Windows/System32/drivers/etc]> █ +[C:/Windows/System32/drivers/etc]> ▁ ``` Directories in [`CD_PATH`](#cd-path) will be also be shortened. @@ -315,7 +409,7 @@ Directories in [`CD_PATH`](#cd-path) will be also be shortened. ```powershell [C:/]> setocd CD_PATH ~/projects [C:/]> cd p..shell -[~/projects/PowerShell/]> █ +[~/projects/PowerShell/]> ▁ ``` [`AUTO_CD`](#auto-cd) uses the same expansion algorithm when enabled. @@ -327,7 +421,7 @@ True [~]> /w/s/d/et [C:/Windows/System32/drivers/etc]> ~/pr/pow/src [~/projects/PowerShell/src]> .sdk -[~/projects/PowerShell/src/Microsoft.PowerShell.SDK]> █ +[~/projects/PowerShell/src/Microsoft.PowerShell.SDK]> ▁ ``` @@ -341,16 +435,16 @@ if enabled. [C:/Windows/System32/drivers/etc]> cd ... # same as `up 2` or `.. 2` [C:/Windows/System32]> cd- [C:/Windows/System32/drivers/etc>] cd .... # same as `up 3` or `.. 3` -[C:/Windows]> █ +[C:/Windows]> ▁ ``` ## No argument `cd` If the _`NOARG_CD`_ [option](#configure) is defined then `cd` without arguments navigates into that -directory (`~` by default). This overrides the out of the box behaviour on PowerShell>=6.0, where -no-arg `cd` _always_ navigates to `~` and PowerShell < 6.0, where no-argument `cd` does nothing at -all. +directory (`~` by default). This overrides the out of the box behaviour of PowerShell >=6.0, where +no-arg `cd` _always_ navigates to `~` and of PowerShell < 6.0, where no-argument `cd` does nothing +at all. ```powershell [~/projects/powershell]> cd @@ -371,7 +465,7 @@ You can also use the alias `cd:` or the explicit `ReplaceWith` parameter of `Set [~/Modules/Unix/Microsoft.PowerShell.Utility]> cd unix shared [~/Modules/Shared/Microsoft.PowerShell.Utility]> cd: -Replace shared -With unix [~/Modules/Unix/Microsoft.PowerShell.Utility]> cd unix -ReplaceWith shared -[~/Modules/Shared/Microsoft.PowerShell.Utility]> █ +[~/Modules/Shared/Microsoft.PowerShell.Utility]> ▁ ``` @@ -397,17 +491,19 @@ Paths within [`$cde.CD_PATH`](#cd-path) are included in the completion results. ```powershell [~]> $cde.CD_PATH += '~\Documents\' [~]> cd win/mod⇥ -[~]> ~\Documents\WindowsPowerShell\Modules\█ +[~]> ~\Documents\WindowsPowerShell\Modules\▁ ``` :point_right: The total number of completions offered is limited by the `MaxCompletions` [option](#configure) -(or calculated dynamically to fit the screen if `MaxCompletions` is falsy). Although the completions -are sorted by type (folders first) and then by name for ease of reading, that sort is applied _after_ -the limit has been applied to the original results. Those results are sorted breadth first for -responsiveness. +or calculated dynamically to fit the screen if `MaxCompletions` is falsy. Although the completions +are sorted by type (folders first) and then by name for ease of reading, that sort is applied +_after_ the limit has been applied to the original results. Those original results are sorted +breadth first in order to keep the completion as responsive as possible. -_A console beep is emitted in cases where the available results have been truncated._ +:point_right: +If the number of available completions is greater than `MaxCompletions`, causing the list to be +truncated, then that is noted in the completion tooltip by default. ## Single and double periods @@ -417,7 +513,7 @@ segment containing `.sdk` is expanded into `*.sdk*`. ```powershell [~]> cd proj/pow/s/.sdk⇥ -[~]> cd ~\projects\powershell\src\Microsoft.PowerShell.SDK\█ +[~]> cd ~\projects\powershell\src\Microsoft.PowerShell.SDK\▁ ``` or @@ -436,7 +532,7 @@ A double-dot (`..`) token is expanded inside, so `s..32` becomes `s*32`. ```powershell [~]> ls /w/s..32⇥ -[~]> ls C:\Windows\System32\█ +[~]> ls C:\Windows\System32\▁ ``` @@ -446,7 +542,7 @@ The [multi-dot syntax](#multi-dot-cd) provides tab completion into ancestor dire ```powershell [~/projects/powershell/docs/git]> cd ...⇥ -[~/projects/powershell/docs/git]> cd ~\projects\powershell\█ +[~/projects/powershell/docs/git]> cd ~\projects\powershell\▁ ``` ```powershell @@ -495,10 +591,10 @@ directories to get to the file you're looking for.) ```powershell [~]> setocd DirCompletions md [~]> md ~/pow/src⇥ -[~]> md ~\powershell\src\█ +[~]> md ~\powershell\src\▁ [~]> setocd PathCompletions Copy-Item [~]> cp /t/⇥ -[~]> cp C:\temp\subdir\█ +[~]> cp C:\temp\subdir\▁ subdir txtFile.txt txtFile2.txt ────── @@ -515,7 +611,7 @@ need to provide a wrapper. Either the wrapper or the target itself should handle [~]> setocd PathCompletions Invoke-VSCode [~]> Set-Alias co Invoke-VSCode [~]> co ~/pr/po⇥ -[~]> co ~\projects\powershell\█ +[~]> co ~\projects\powershell\▁ ``` An alternative to registering a bunch of aliases is to create a tiny wrapper to pipe input @@ -573,7 +669,7 @@ _ColorCompletion_ is off by default. Enable it on with `setocd ColorCompletion`. [~]> projects [~/projects]> cd-extras [~/projects/cd-extras]> / -[C:/]> █ +[C:/]> ▁ ``` As with the [enhanced `cd`](#cd-enhancements) command, [abbreviated paths](#path-shortening) @@ -584,7 +680,7 @@ and [multi-dot syntax](#multi-dot-cd) are supported. [~/projects]> cd-e [~/projects/cd-extras]> cd [~]> pr/cd -[~/projects/cd-extras]> █ +[~/projects/cd-extras]> ▁ ``` @@ -607,7 +703,7 @@ n Name Path [C:/temp]> ~2 # or ~ 2 [C:/Windows/System32]> ~~2 # or ~~ 2 -[C:/temp]> █ +[C:/temp]> ▁ ``` @@ -619,7 +715,7 @@ n Name Path [C:/Windows/System32/drivers/etc]> ... # same as `up 2` or `.. 2` [C:/Windows/System32]> cd- [C:/Windows/System32/drivers/etc>] .... # same as `up 3` or `.. 3` -[C:/Windows]> █ +[C:/Windows]> ▁ ``` @@ -631,7 +727,7 @@ n Name Path [~]> setocd CD_PATH ~/documents [~]> # or $cde.CD_PATH = ,'~/documents' [~]> cd WindowsPowerShell -[~/documents/WindowsPowerShell]> █ +[~/documents/WindowsPowerShell]> ▁ ``` [Tab-completion](#enhanced-completion-for-cd-and-others), [path shortening](#path-shortening) and @@ -655,7 +751,7 @@ prefer the former. [~]> resolve-path someDir | setocd CD_PATH [~]> cd child [~/child]> cd child -[~/someDir/child]> █ +[~/someDir/child]> ▁ ``` :point_right: @@ -684,7 +780,7 @@ CDABLE_VARS is off by default; enable it with, [`setocd CDABLE_VARS`](#configure [~/projects/powershell]> $bk1 = $pwd [~/projects/powershell]> cd [~]> cd bk1 -[~/projects/powershell]> █ +[~/projects/powershell]> ▁ ``` It works with relative paths too, so if you find yourself frequently `cd`ing into the same @@ -693,7 +789,7 @@ subdirectories you could create a corresponding variable. ```powershell [~/projects/powershell]> $gh = './.git/hooks' [~/projects/powershell]> cd gh -[~/projects/powershell/.git/hooks]> █ +[~/projects/powershell/.git/hooks]> ▁ ``` You can combine it with [AUTO_CD](#auto-cd) for great good: @@ -702,11 +798,11 @@ You can combine it with [AUTO_CD](#auto-cd) for great good: [C:/projects/powershell/src/Modules/Unix]> xup -Export | out-null [C:/projects/powershell/src/Modules/Unix]> projects [C:/projects]> src -[C:/projects/powershell/src]> █ +[C:/projects/powershell/src]> ▁ ``` -# Additional helpers +# Related commands ## Get-Up (gup) @@ -777,7 +873,7 @@ other providers too. [HKLM:/SOFTWARE/Microsoft/Windows/CurrentVersion/WindowsUpdate]> .. [HKLM:/SOFTWARE/Microsoft/Windows/CurrentVersion]> cd- [HKLM:/SOFTWARE/Microsoft/Windows/CurrentVersion/WindowsUpdate]> cd- 2 -[~]> █ +[~]> ▁ ``` # Install @@ -901,7 +997,7 @@ then you'll probably want to restore the original `cd` alias too. [~]> set-alias cde set-locationex [~]> cde /w/s/d/et [C:/Windows/System32/drivers/etc]> cd- # still cd-, not cde- -[~]> █ +[~]> ▁ ``` :point_right: @@ -912,9 +1008,10 @@ then you'll probably want to restore the original `cd` alias too. [~]> Set-Location code [~/code]> cd- -[~/code]> █ +[~/code]> ▁ ``` [0]: https://github.com/PowerShell/PSReadLine [1]: https://github.com/DHowett/DirColors [2]: https://docs.microsoft.com/powershell/module/psreadline/set-psreadlinekeyhandler +[3]: https://github.com/ajeetdsouza/zoxide/wiki/Algorithm#maching From e68ad7fb571290de7c5ee5af8557b4b9612c3492 Mon Sep 17 00:00:00 2001 From: Nick Date: Sat, 2 Jul 2022 11:38:55 +1000 Subject: [PATCH 07/16] Ensure type names PS5 compatible. --- cd-extras/private/CompleteFrecent.ps1 | 2 +- cd-extras/private/CompletePaths.ps1 | 16 +++++----------- cd-extras/private/Core.ps1 | 11 ++++++----- cd-extras/public/Expand-Path.ps1 | 2 +- cd-extras/public/Get-Bookmark.ps1 | 2 +- cd-extras/public/Get-FrecentLocation.ps1 | 2 +- cd-extras/public/Get-RecentLocation.ps1 | 2 +- cd-extras/public/Set-FrecentLocation.ps1 | 4 ++-- cd-extras/public/Set-RecentLocation.ps1 | 4 ++-- cd-extras/public/_Classes.ps1 | 15 ++++++++------- 10 files changed, 28 insertions(+), 32 deletions(-) diff --git a/cd-extras/private/CompleteFrecent.ps1 b/cd-extras/private/CompleteFrecent.ps1 index 5a0f811..1c6cb95 100644 --- a/cd-extras/private/CompleteFrecent.ps1 +++ b/cd-extras/private/CompleteFrecent.ps1 @@ -6,6 +6,6 @@ function CompleteFrecent { if (!$recents) { return } @($recents) | Where Path -match ($wordToComplete | RemoveSurroundingQuotes | RemoveTrailingSeparator | Escape) | - IndexedComplete $false | + IndexedComplete -indexed $false | DefaultIfEmpty { $null } } diff --git a/cd-extras/private/CompletePaths.ps1 b/cd-extras/private/CompletePaths.ps1 index 46b00fe..8f3741c 100644 --- a/cd-extras/private/CompletePaths.ps1 +++ b/cd-extras/private/CompletePaths.ps1 @@ -34,15 +34,10 @@ function CompletePaths { Process { $fullPath = $_ | Convert-Path - $completionText = if ($wordToComplete -match '^\.{1,2}$') { - $wordToComplete - } - elseif (!($wordToComplete | IsRooted) -and ($_ | Resolve-Path -Relative | IsDescendedFrom ..)) { - $_ | Resolve-Path -Relative - } - else { - $fullPath -replace "^$($HOME | NormaliseAndEscape)", "~" - } + $completionText = + if ($wordToComplete -in '.', '..') { $wordToComplete } + elseif (!($wordToComplete | IsRooted) -and ($relative = $_ | Resolve-Path -Relative) -and ($relative | IsDescendedFrom ..)) { $relative } + else { $fullPath -replace "^$($HOME | NormaliseAndEscape)", "~" } # add normalised trailing directory separator; quote if contains spaces $trailChar = if ($_.PSIsContainer) { ${/} } @@ -115,13 +110,12 @@ function CompletePaths { Expand-Path @switches ($wordToExpand -replace $Matches[0], $maybeVar) } - $allCompletions = @($completions) + @($variableCompletions) | ? { $_ } + $allCompletions = (@($completions) + @($variableCompletions)).Where{$_} | select -Unique $isListTruncated = if ($allCompletions.Length -gt $maxCompletions) { $true } if (!$allCompletions) { return } $allCompletions | - Select -Unique | Sort-Object { !$_.PSIsContainer, $_.PSChildName } | Select -First $maxCompletions | CompletionResult $isListTruncated diff --git a/cd-extras/private/Core.ps1 b/cd-extras/private/Core.ps1 index 5345de3..b4cc619 100644 --- a/cd-extras/private/Core.ps1 +++ b/cd-extras/private/Core.ps1 @@ -1,10 +1,11 @@ ${Script:/} = [IO.Path]::DirectorySeparatorChar +$Script:esc = [char]27 # for PS <7 $Script:undoStack = [Collections.Stack]::new() $Script:redoStack = [Collections.Stack]::new() $Script:recent = [Collections.Generic.Dictionary[string, RecentDir]]::new() $Script:logger = { Write-Verbose ($args[0] | ConvertTo-Json) } -$Script:background +$Script:background = $null function DefaultIfEmpty([scriptblock] $default) { Begin { $any = $false } @@ -15,7 +16,7 @@ function DefaultIfEmpty([scriptblock] $default) { filter Truncate([int] $maxLength = $cde.MaxMenuLength) { if (!$_ -or $_.Length -le $maxLength) { return $_ } - if ($_.StartsWith([char]27)) { + if ($_.StartsWith($esc)) { TruncatedColoured $_ $maxLength } else { @@ -32,7 +33,7 @@ function TruncatedColoured([string]$string, $maxLen) { $string } else { - $string.Substring(0, $textStart) + ($text | Truncate) + "$([char]27)[0m" + $string.Substring(0, $textStart) + ($text | Truncate) + "$esc[0m" } } @@ -75,7 +76,7 @@ filter SurroundAndTerminate($trailChar) { } filter RemoveTrailingSeparator { - $_ -replace "[/\\]$", '' + if ($_ -match '[/\\].*?([/\\])$') { $_.TrimEnd('/', '\') } else { $_ } } filter EscapeWildcards { @@ -193,7 +194,7 @@ function RecentsByTermWithSort([int] $first, [string[]] $terms, [scriptblock] $s } function GetFrecent([int] $first, [string[]] $terms) { - function FrecencyFactor([ulong] $lastEntered) { + function FrecencyFactor([uint64] $lastEntered) { $now = [System.DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() if ($lastEntered -gt ($now - 1000 * 60 * 60)) { 4 } # past hour diff --git a/cd-extras/public/Expand-Path.ps1 b/cd-extras/public/Expand-Path.ps1 index 961a366..3a2b47d 100644 --- a/cd-extras/public/Expand-Path.ps1 +++ b/cd-extras/public/Expand-Path.ps1 @@ -58,7 +58,7 @@ function Expand-Path { [Parameter(ValueFromPipeline, Mandatory)] [SupportsWildcards()] [string] $Path, - [ushort] $MaxResults = [ushort]::MaxValue, + [uint16] $MaxResults = [uint16]::MaxValue, [string[]] $SearchPaths = $cde.CD_PATH, [char[]] $WordDelimiters = $cde.WordDelimiters, [switch] $File, diff --git a/cd-extras/public/Get-Bookmark.ps1 b/cd-extras/public/Get-Bookmark.ps1 index 7ff85ce..32e3874 100644 --- a/cd-extras/public/Get-Bookmark.ps1 +++ b/cd-extras/public/Get-Bookmark.ps1 @@ -27,7 +27,7 @@ function Get-Bookmark { [OutputType([string[]])] param( - [Parameter(Position = 0)] [ushort] $First = $cde.MaxRecentCompletions + [Parameter(Position = 0)] [uint16] $First = $cde.MaxRecentCompletions ) $recent.Values.Where{ $_.Favour } | Sort-Object EnterCount, LastEntered -Descending | diff --git a/cd-extras/public/Get-FrecentLocation.ps1 b/cd-extras/public/Get-FrecentLocation.ps1 index c203d16..4e26827 100644 --- a/cd-extras/public/Get-FrecentLocation.ps1 +++ b/cd-extras/public/Get-FrecentLocation.ps1 @@ -52,7 +52,7 @@ function Get-FrecentLocation { [OutputType([IndexedPath])] param( - [Parameter(ParameterSetName = 'First')] [ushort] $First = $cde.MaxRecentCompletions, + [Parameter(ParameterSetName = 'First')] [uint16] $First = $cde.MaxRecentCompletions, [Parameter(ValueFromRemainingArguments)] [string[]] $Terms ) diff --git a/cd-extras/public/Get-RecentLocation.ps1 b/cd-extras/public/Get-RecentLocation.ps1 index a5d17fe..9c21ad5 100644 --- a/cd-extras/public/Get-RecentLocation.ps1 +++ b/cd-extras/public/Get-RecentLocation.ps1 @@ -49,7 +49,7 @@ function Get-RecentLocation { [OutputType([IndexedPath])] [CmdletBinding(DefaultParameterSetName = '')] param( - [Parameter(ParameterSetName = 'First')] [ushort] $First = $cde.MaxRecentCompletions, + [Parameter(ParameterSetName = 'First')] [uint16] $First = $cde.MaxRecentCompletions, [Parameter(ValueFromRemainingArguments)] [string[]] $Terms ) diff --git a/cd-extras/public/Set-FrecentLocation.ps1 b/cd-extras/public/Set-FrecentLocation.ps1 index 714e6f4..fd2bfc6 100644 --- a/cd-extras/public/Set-FrecentLocation.ps1 +++ b/cd-extras/public/Set-FrecentLocation.ps1 @@ -70,7 +70,7 @@ function Set-FrecentLocation { [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'n')] param( [Parameter(ParameterSetName = 'n', Position = 0)] - [ushort] $n = 1, + [uint16] $n = 1, [Alias('NamePart')] [Parameter(ParameterSetName = 'named', Position = 0)] @@ -80,7 +80,7 @@ function Set-FrecentLocation { [Parameter(ParameterSetName = 'list', Mandatory)] [switch] $List, [Parameter(ParameterSetName = 'list')] - [ushort] $First = $cde.MaxRecentCompletions, + [uint16] $First = $cde.MaxRecentCompletions, [Parameter(ParameterSetName = 'list', ValueFromRemainingArguments)] [string[]] $ListTerms, diff --git a/cd-extras/public/Set-RecentLocation.ps1 b/cd-extras/public/Set-RecentLocation.ps1 index 70343c0..228d7de 100644 --- a/cd-extras/public/Set-RecentLocation.ps1 +++ b/cd-extras/public/Set-RecentLocation.ps1 @@ -54,7 +54,7 @@ function Set-RecentLocation { [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'n')] param( [Parameter(ParameterSetName = 'n', Position = 0)] - [ushort] $n = 1, + [uint16] $n = 1, [Alias('NamePart')] [Parameter(ParameterSetName = 'named', Position = 0)] @@ -64,7 +64,7 @@ function Set-RecentLocation { [Parameter(ParameterSetName = 'list', Mandatory)] [switch] $List, [Parameter(ParameterSetName = 'list')] - [ushort] $First = $cde.MaxRecentCompletions, + [uint16] $First = $cde.MaxRecentCompletions, [Parameter(ParameterSetName = 'list', ValueFromRemainingArguments)] [string[]] $ListTerms, diff --git a/cd-extras/public/_Classes.ps1 b/cd-extras/public/_Classes.ps1 index 24a2902..3b6c785 100644 --- a/cd-extras/public/_Classes.ps1 +++ b/cd-extras/public/_Classes.ps1 @@ -1,5 +1,5 @@ class IndexedPath { - [ushort] $n + [uint16] $n [string] $Name [string] $Path @@ -8,8 +8,8 @@ class IndexedPath { class RecentDir { [string] $Path - [ulong] $LastEntered - [uint] $EnterCount + [uint64] $LastEntered + [uint32] $EnterCount [bool] $Favour [string] ToString() { return "{0}, {1}, {2}" -f $this.LastEntered, $this.Count, $this.Favour } @@ -18,6 +18,7 @@ class RecentDir { class CdeOptions { hidden [string] $recentHash hidden [Threading.Mutex] $mutex = [Threading.Mutex]::new($false, 'cde.RECENT_DIRS_FILE') + hidden [array] $executableEx = $env:PATHEXT -split ';' [bool] $AUTO_CD = $true [bool] $CDABLE_VARS = $false @@ -26,10 +27,10 @@ class CdeOptions { [string] $RECENT_DIRS_FILE = $null [string[]] $RECENT_DIRS_EXCLUDE = @() [bool] $RecentDirsFallThrough = $true - [ushort] $MaxRecentDirs = 120 - [ushort] $MaxRecentCompletions = 60 - [ushort] $MaxCompletions = 0 - [ushort] $MaxMenuLength = 36 + [uint16] $MaxRecentDirs = 120 + [uint16] $MaxRecentCompletions = 60 + [uint16] $MaxCompletions = 0 + [uint16] $MaxMenuLength = 36 [char[]] $WordDelimiters = '.', '_', '-' [string[]] $DirCompletions = @('Set-Location', 'Set-LocationEx', 'Push-Location') [string[]] $PathCompletions = @('Get-ChildItem', 'Get-Item', 'Invoke-Item', 'Expand-Path') From 03f821b46ab5a9272421798ec45cd3cd28b3f017 Mon Sep 17 00:00:00 2001 From: Nick Date: Sat, 2 Jul 2022 11:39:19 +1000 Subject: [PATCH 08/16] Fix issue with truncation of colourised completions. --- cd-extras/private/Core.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cd-extras/private/Core.ps1 b/cd-extras/private/Core.ps1 index b4cc619..34eb126 100644 --- a/cd-extras/private/Core.ps1 +++ b/cd-extras/private/Core.ps1 @@ -26,8 +26,9 @@ filter Truncate([int] $maxLength = $cde.MaxMenuLength) { function TruncatedColoured([string]$string, $maxLen) { $textStart = $string.IndexOf('m') + 1 - $startFinalEscapeSequence = $string.LastIndexOf([char]27) - $text = $string.Substring($textStart, $startFinalEscapeSequence - $textStart) + $startFinalEscapeSequence = $string.LastIndexOf($esc) + $textEnd = if ($startFinalEscapeSequence -gt $textStart) { $startFinalEscapeSequence } else {$string.Length - 1} + $text = $string.Substring($textStart, $textEnd - $textStart) if ($text.Length -le $maxLen) { $string From 4d32a8d579bb41132c1dad93066d8e8c62f94805 Mon Sep 17 00:00:00 2001 From: Nick Date: Sat, 2 Jul 2022 11:39:39 +1000 Subject: [PATCH 09/16] Add ability to set multiple options at the same time. --- cd-extras/cd-extras.psm1 | 8 +---- cd-extras/public/Set-CdExtrasOption.ps1 | 45 +++++++++++++++---------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/cd-extras/cd-extras.psm1 b/cd-extras/cd-extras.psm1 index 1c12dc4..8f6bbf1 100644 --- a/cd-extras/cd-extras.psm1 +++ b/cd-extras/cd-extras.psm1 @@ -7,13 +7,7 @@ Get-ChildItem -File -Filter *.ps1 $PSScriptRoot/private, $PSScriptRoot/public | # remove stupid phantom module Get-Module | Where Path -eq ("$PSScriptRoot/public/_Classes.ps1" | Resolve-Path) | Remove-Module -$global:cde = if ((Test-Path variable:cde) -and $cde -is [System.Collections.IDictionary]) { - [CdeOptions]$cde -} -else { - [CdeOptions]::new() -} - +$global:cde = [CdeOptions]::new() (Get-Variable cde).Attributes.Add([ValidateScript]::new( { Set-CdExtrasOption -Validate } )) RegisterCompletions @('Step-Up') 'n' { CompleteAncestors @args } diff --git a/cd-extras/public/Set-CdExtrasOption.ps1 b/cd-extras/public/Set-CdExtrasOption.ps1 index a422838..b393194 100644 --- a/cd-extras/public/Set-CdExtrasOption.ps1 +++ b/cd-extras/public/Set-CdExtrasOption.ps1 @@ -8,6 +8,9 @@ The option to update. .PARAMETER Value The new value. +.PARAMETER Options +A dictionary of options and values to update. + .EXAMPLE PS C:\> setocd AUTO_CD @@ -31,15 +34,18 @@ function Set-CdExtrasOption { [ArgumentCompleter( { $global:cde | Get-Member -Type Property -Name "$($args[2])*" | % Name })] [Parameter(ParameterSetName = 'Set', Mandatory, Position = 0)] [string] $Option, - [Parameter(ParameterSetName = 'Set', Position = 1, ValueFromPipeline)] $Value, + [Parameter(ParameterSetName = 'SetMany', Mandatory, Position = 0)] + [Collections.IDictionary] $Options, + [Parameter(ParameterSetName = 'Validate', Mandatory)] [switch] $Validate ) - if ($PSCmdlet.ParameterSetName -eq 'Set') { + # first update the $cde variable per the given settings + if ($PSCmdlet.ParameterSetName -eq 'Set' -or $PSCmdlet.ParameterSetName -eq 'SetMany') { $flags = @( 'AUTO_CD' @@ -49,42 +55,44 @@ function Set-CdExtrasOption { 'RecentDirsFallThrough' ) - if ($null -eq $Value -and $Option -in $flags) { - $Value = $true - } - $completionTypes = @( 'PathCompletions' 'DirCompletions' 'FileCompletions' ) - if ($Option -in $completionTypes) { - if ($Global:cde.$option -notcontains $value) { - $value | Where { $Global:cde.$option -notcontains $_ } | % { $Global:cde.$option += $_ } - } + if ($null -eq $Value -and $Option -in $flags) { + $Value = $true } - else { - $Global:cde.$option = $value + + $opts = if ($Option) { $Option } else { $Options.Keys } + $opts | % { + $opt = $_ + $val = if ($Options -is [hashtable] -and $Options.Keys -contains $_) { $Options[$_] } else { $Value } + + if ($opt -in $completionTypes) { + if ($Global:cde.$opt -notcontains $val) { + $Global:cde.$opt += $val + } + } + else { + $Global:cde.$opt = $val + } } } + # then perform various side effects based on the current settings if ($cde.RECENT_DIRS_FILE) { $path = $cde.RECENT_DIRS_FILE -replace '~', $HOME $cde.RECENT_DIRS_FILE = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath( $path ) # save recent dirs from memory when dirs file set - if ($recent.Count) { PersistRecent } + if ($recent.Count -or !(Test-Path $cde.RECENT_DIRS_FILE)) { PersistRecent } # load recent dirs into memory at startup elseif (Test-Path $cde.RECENT_DIRS_FILE) { ImportRecent } - - else { - Add-Content $cde.RECENT_DIRS_FILE '' - $global:cde.recentHash = (Get-FileHash $cde.RECENT_DIRS_FILE).Hash.ToString() - } } $cde.RECENT_DIRS_EXCLUDE = $cde.RECENT_DIRS_EXCLUDE.ForEach{ Resolve-Path $_ } @@ -108,5 +116,6 @@ function Set-CdExtrasOption { } # can be used to ensure side effects have run without actually changing any options + # this is used when the $cde variable is updated irectly if ($Validate) { return $true } } From 97cbb95d26ca322dbf1bc032d343eb24c3243bfa Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 21 Feb 2023 22:57:02 +1100 Subject: [PATCH 10/16] Fix up some failing test cases. --- cd-extras/private/Core.ps1 | 9 +++++---- cd-extras/public/Get-Ancestors.ps1 | 2 +- cd-extras/public/Set-CdExtrasOption.ps1 | 3 ++- tests/cd-extras.Tests.ps1 | 18 +++++++++++------- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/cd-extras/private/Core.ps1 b/cd-extras/private/Core.ps1 index 34eb126..90e904d 100644 --- a/cd-extras/private/Core.ps1 +++ b/cd-extras/private/Core.ps1 @@ -27,7 +27,7 @@ filter Truncate([int] $maxLength = $cde.MaxMenuLength) { function TruncatedColoured([string]$string, $maxLen) { $textStart = $string.IndexOf('m') + 1 $startFinalEscapeSequence = $string.LastIndexOf($esc) - $textEnd = if ($startFinalEscapeSequence -gt $textStart) { $startFinalEscapeSequence } else {$string.Length - 1} + $textEnd = if ($startFinalEscapeSequence -gt $textStart) { $startFinalEscapeSequence } else { $string.Length - 1 } $text = $string.Substring($textStart, $textEnd - $textStart) if ($text.Length -le $maxLen) { @@ -128,7 +128,7 @@ function IndexPaths( $rootLabel = 'root' # this on happens on *nix ) { $xs = $xs -ne '' | Select -Unique - if (!$xs) { return } + if (!$xs) { return @() } $i = 0 $xs.ForEach{ @@ -138,6 +138,7 @@ function IndexPaths( Path = $_ } } } + function RegisterCompletions([string[]] $commands, $param, $target) { Register-ArgumentCompleter -CommandName $commands -ParameterName $param -ScriptBlock $target } @@ -157,7 +158,7 @@ function ImportRecent() { } function RefreshRecent() { - if (!$cde.RECENT_DIRS_FILE) { return } + if (!$cde.RECENT_DIRS_FILE -or !(Test-Path $cde.RECENT_DIRS_FILE)) { return } try { if ($hasMutex = $cde.mutex.WaitOne(1)) { @@ -189,7 +190,7 @@ function RecentsByTermWithSort([int] $first, [string[]] $terms, [scriptblock] $s } RefreshRecent - $recent.Values.Where( { ($_.Path -ne $pwd) -and (MatchesTerms $_.Path) }) | + $recent.Values.Where( { ($_.Path -ne ($pwd | RemoveTrailingSeparator)) -and (MatchesTerms $_.Path) }) | Sort-Object $sort -Descending | select -First $first -Expand Path } diff --git a/cd-extras/public/Get-Ancestors.ps1 b/cd-extras/public/Get-Ancestors.ps1 index b62afee..fb9596a 100644 --- a/cd-extras/public/Get-Ancestors.ps1 +++ b/cd-extras/public/Get-Ancestors.ps1 @@ -60,7 +60,7 @@ function Get-Ancestors { # this works around registry provider having a root that can't be easily navigated to $root = if ($start.Provider.VolumeSeparatedByColon) { "$($start.Drive.Name):${/}" } else { $start.Drive.Root } - if (!$start -or ($start.Path -eq $root)) { return } + if (!$start -or ($start.Path -eq $root)) { return [IndexedPath[]]@() } $next = $start.Path $paths = @(while ($next -and ($next = $next | Split-Path) -and ($next -ne $root)) { $next }) diff --git a/cd-extras/public/Set-CdExtrasOption.ps1 b/cd-extras/public/Set-CdExtrasOption.ps1 index b393194..1f7d898 100644 --- a/cd-extras/public/Set-CdExtrasOption.ps1 +++ b/cd-extras/public/Set-CdExtrasOption.ps1 @@ -83,11 +83,12 @@ function Set-CdExtrasOption { # then perform various side effects based on the current settings if ($cde.RECENT_DIRS_FILE) { + $path = $cde.RECENT_DIRS_FILE -replace '~', $HOME $cde.RECENT_DIRS_FILE = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath( $path ) # save recent dirs from memory when dirs file set - if ($recent.Count -or !(Test-Path $cde.RECENT_DIRS_FILE)) { PersistRecent } + if ($recent.Count -gt 1 -or !(Test-Path $cde.RECENT_DIRS_FILE)) { PersistRecent } # load recent dirs into memory at startup elseif (Test-Path $cde.RECENT_DIRS_FILE) { diff --git a/tests/cd-extras.Tests.ps1 b/tests/cd-extras.Tests.ps1 index 3df7289..f4107d8 100644 --- a/tests/cd-extras.Tests.ps1 +++ b/tests/cd-extras.Tests.ps1 @@ -299,6 +299,9 @@ Describe 'cd-extras' { } setocd RECENT_DIRS_FILE './recent_dirs' + Start-Sleep -Milliseconds 50 + Get-RecentLocation | Should -BeNullOrEmpty + $newList.Values | Export-Csv -LiteralPath $cde.RECENT_DIRS_FILE Get-RecentLocation | Should -Not -BeNullOrEmpty @@ -351,6 +354,7 @@ Describe 'cd-extras' { (cdf -l) | Should -BeNullOrEmpty Get-BookMark | Should -BeNullOrEmpty + cd TestDrive:/powershell/tools/terms cdf -m Get-Bookmark | Should -Be $PWD.Path cdf -u @@ -865,9 +869,7 @@ Describe 'cd-extras' { cd .. cd .. - $undos = Get-Stack -Undo - $undos | where n -eq 1 | % Path | Should -Be ( - $undos | where n -eq 3 | % Path) + (Get-Stack -Undo).Count | Should -Be (3) } It 'shows the redo stack' { @@ -1244,19 +1246,21 @@ Describe 'cd-extras' { cd TestDrive:/powershell/tools/ResxGen cd TestDrive:/ - setocd RECENT_DIRS_FILE TestDrive:/recent_dirs - Start-Sleep -Milliseconds 10 # save is async - (Get-Content TestDrive:/recent_dirs).Length | Should -Be 4 # including header + $tmp = New-TemporaryFile + setocd RECENT_DIRS_FILE $tmp + Start-Sleep -Milliseconds 50 # save is async + (Get-Content $tmp).Length | Should -Be 4 # including header setocd RECENT_DIRS_FILE Remove-RecentLocation * Get-RecentLocation | Should -BeNullOrEmpty - setocd RECENT_DIRS_FILE TestDrive:/recent_dirs + setocd RECENT_DIRS_FILE $tmp (Get-RecentLocation).Count | Should -Be 2 Remove-RecentLocation * Test-Path TestDrive:/recent_dirs_2 | Should -Be $false setocd RECENT_DIRS_FILE TestDrive:/recent_dirs_2 + Start-Sleep -Milliseconds 50 # save is async Test-Path TestDrive:/recent_dirs_2 | Should -Be $true setocd RECENT_DIRS_FILE From e92b5b62bc14fa8f25869212f50c81325b3bdd8e Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 21 Feb 2023 23:13:00 +1100 Subject: [PATCH 11/16] Fix coverage paths for Pester v5. --- .github/workflows/onPush.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/onPush.yaml b/.github/workflows/onPush.yaml index 5ce95a6..6b284a0 100644 --- a/.github/workflows/onPush.yaml +++ b/.github/workflows/onPush.yaml @@ -17,10 +17,11 @@ jobs: . ./cd-extras/public/_Classes.ps1 md ./_reports Import-Module Pester - Invoke-Pester ./tests/cd-extras.Tests.ps1 -EnableExit -CodeCoverage ./cd-extras/private/*.ps1,./cd-extras/public/*.ps1 -CodeCoverageOutputFile ./_reports/coverage.xml -OutputFile ./_reports/testresults.xml + cd tests + Invoke-Pester ./cd-extras.Tests.ps1 -EnableExit -CodeCoverage ../cd-extras/private/*.ps1,../cd-extras/public/*.ps1 -CodeCoverageOutputFile ../_reports/coverage.xml -OutputFile ../_reports/testresults.xml shell: pwsh - # run the tests again on WIndows, this time in PowerShell v5 + # run the tests again on Windows, this time in PowerShell v5 - name: Run the tests (Powershell v5) if: matrix.os == 'windows-latest' run: | From 8d50a9c889a111eb059055c7aa530d8987c08bb8 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 21 Feb 2023 23:36:11 +1100 Subject: [PATCH 12/16] Exclude test on legacy Windows PowerShell. --- tests/cd-extras.Tests.ps1 | 2 +- tests/test.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/cd-extras.Tests.ps1 b/tests/cd-extras.Tests.ps1 index f4107d8..effa24b 100644 --- a/tests/cd-extras.Tests.ps1 +++ b/tests/cd-extras.Tests.ps1 @@ -282,7 +282,7 @@ Describe 'cd-extras' { Get-RecentLocation | select -Expand Name | Should -Be 'terms', 'ResxGen' } - It 'refreshes the list if RECENT_DIRS_FILE updated in another process' { + It 'refreshes the list if RECENT_DIRS_FILE updated in another process' -Skip:($PSEdition -eq 'Desktop') { Get-RecentLocation | Should -BeNullOrEmpty $fst, $snd = ((Resolve-Path TestDrive:/), (Resolve-Path TestDrive:/powershell)).Path diff --git a/tests/test.ps1 b/tests/test.ps1 index 3cfee69..6928186 100644 --- a/tests/test.ps1 +++ b/tests/test.ps1 @@ -2,7 +2,7 @@ param ([switch] $Cover) if ($Cover) { $ex = "$PSScriptRoot/../cd-extras" - Invoke-Pester $PSScriptRoot\cd-extras.Tests.ps1 -CodeCoverage "$ex/public/*.ps1", "$ex/private/*.ps1" @args + Invoke-Pester $PSScriptRoot\cd-extras.Tests.ps1 -CodeCoverage "$ex/public/*.ps1", "$ex/private/*-*.ps1" @args } else { Invoke-Pester $PSScriptRoot\cd-extras.Tests.ps1 @args From 6c3465dd6efef96f8958765fb26569b2afcacd82 Mon Sep 17 00:00:00 2001 From: Nick Date: Wed, 22 Feb 2023 09:15:03 +1100 Subject: [PATCH 13/16] Fixes. --- cd-extras/private/Core.ps1 | 23 ++++++----- cd-extras/public/Set-CdExtrasOption.ps1 | 6 +-- tests/cd-extras.Tests.ps1 | 52 +++++++++++++------------ tests/test.ps1 | 2 +- 4 files changed, 43 insertions(+), 40 deletions(-) diff --git a/cd-extras/private/Core.ps1 b/cd-extras/private/Core.ps1 index 90e904d..03cd50c 100644 --- a/cd-extras/private/Core.ps1 +++ b/cd-extras/private/Core.ps1 @@ -149,12 +149,13 @@ function ImportRecent() { $h.Hash.ToString() } - $recent.Clear() $dirs.ForEach{ $dir = [RecentDir]$_ $dir.Favour = $_.Favour -and [bool]::Parse($_.Favour) $recent[$_.Path] = $dir } + + TrimRecent } function RefreshRecent() { @@ -162,7 +163,6 @@ function RefreshRecent() { try { if ($hasMutex = $cde.mutex.WaitOne(1)) { - # assumes we already know the file exists $currentHash = (Get-FileHash -LiteralPath $cde.RECENT_DIRS_FILE).Hash.ToString() if ($currentHash -ne $cde.recentHash) { WriteLog ($currentHash, $cde.recentHash) @@ -236,13 +236,7 @@ function UpdateRecent($path, $favour = $false) { $recent[$path] = $entry - if ($recent.Count -gt $cde.MaxRecentDirs) { - RemoveRecent ( - $recent.Values | - Sort-Object Favour, LastEntered | - select -First ($recent.Count - $cde.MaxRecentDirs) -expand Path) - } - + TrimRecent PersistRecent } @@ -259,8 +253,17 @@ function RemoveRecent([string[]] $dirs) { PersistRecent } +function TrimRecent() { + if ($recent.Count -gt $cde.MaxRecentDirs) { + RemoveRecent ( + $recent.Values | + Sort-Object Favour, LastEntered | + select -First ($recent.Count - $cde.MaxRecentDirs) -expand Path) + } +} + function PersistRecent() { - if ($cde.RECENT_DIRS_FILE) { + if ($cde.RECENT_DIRS_FILE -and $recent.Count) { if (!$background) { InitRunspace } try { diff --git a/cd-extras/public/Set-CdExtrasOption.ps1 b/cd-extras/public/Set-CdExtrasOption.ps1 index 1f7d898..5d560ad 100644 --- a/cd-extras/public/Set-CdExtrasOption.ps1 +++ b/cd-extras/public/Set-CdExtrasOption.ps1 @@ -88,12 +88,10 @@ function Set-CdExtrasOption { $cde.RECENT_DIRS_FILE = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath( $path ) # save recent dirs from memory when dirs file set - if ($recent.Count -gt 1 -or !(Test-Path $cde.RECENT_DIRS_FILE)) { PersistRecent } + if (!(Test-Path $cde.RECENT_DIRS_FILE)) { PersistRecent } # load recent dirs into memory at startup - elseif (Test-Path $cde.RECENT_DIRS_FILE) { - ImportRecent - } + elseif (Test-Path $cde.RECENT_DIRS_FILE) { ImportRecent } } $cde.RECENT_DIRS_EXCLUDE = $cde.RECENT_DIRS_EXCLUDE.ForEach{ Resolve-Path $_ } diff --git a/tests/cd-extras.Tests.ps1 b/tests/cd-extras.Tests.ps1 index effa24b..22e4789 100644 --- a/tests/cd-extras.Tests.ps1 +++ b/tests/cd-extras.Tests.ps1 @@ -6,11 +6,8 @@ BeforeDiscovery { } Remove-Module cd-extras -ErrorAction Ignore - - # $Script:xcde = if (Test-Path variable:cde) { $cde } - # $Global:cde = [CdeOptions]@{} Push-Location $PSScriptRoot - Import-Module ../cd-extras/cd-extras.psd1 -Force + Import-Module ../cd-extras/cd-extras.psd1 -Force -ErrorAction Stop } AfterAll { @@ -282,27 +279,24 @@ Describe 'cd-extras' { Get-RecentLocation | select -Expand Name | Should -Be 'terms', 'ResxGen' } - It 'refreshes the list if RECENT_DIRS_FILE updated in another process' -Skip:($PSEdition -eq 'Desktop') { + It 'refreshes the list if RECENT_DIRS_FILE updated in another process' { Get-RecentLocation | Should -BeNullOrEmpty - $fst, $snd = ((Resolve-Path TestDrive:/), (Resolve-Path TestDrive:/powershell)).Path - $newList = [Collections.Generic.Dictionary[string, RecentDir]]::new() - $newList[$fst] = @{ - Path = $fst - LastEntered = [System.DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() - EnterCount = 1 - } - $newList[$snd] = @{ - Path = $snd - LastEntered = [System.DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() - EnterCount = 1 + $items = ((Resolve-Path TestDrive:/), (Resolve-Path TestDrive:/powershell)).Path + $newList = + $items | % { + [RecentDir]@{ + Path = $_ + LastEntered = [System.DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() + EnterCount = 1 + } } setocd RECENT_DIRS_FILE './recent_dirs' Start-Sleep -Milliseconds 50 Get-RecentLocation | Should -BeNullOrEmpty - $newList.Values | Export-Csv -LiteralPath $cde.RECENT_DIRS_FILE + $newList | Export-Csv -LiteralPath $cde.RECENT_DIRS_FILE Get-RecentLocation | Should -Not -BeNullOrEmpty setocd RECENT_DIRS_FILE @@ -1246,22 +1240,30 @@ Describe 'cd-extras' { cd TestDrive:/powershell/tools/ResxGen cd TestDrive:/ - $tmp = New-TemporaryFile - setocd RECENT_DIRS_FILE $tmp + $file = 'TestDrive:/recent_dirs' + setocd RECENT_DIRS_FILE $file Start-Sleep -Milliseconds 50 # save is async - (Get-Content $tmp).Length | Should -Be 4 # including header + + # there should be three entries, so four lines including header + (Get-Content $file).Length | Should -Be 4 setocd RECENT_DIRS_FILE Remove-RecentLocation * Get-RecentLocation | Should -BeNullOrEmpty - setocd RECENT_DIRS_FILE $tmp - (Get-RecentLocation).Count | Should -Be 2 + + cd powershell/tools/packaging/macos + setocd RECENT_DIRS_FILE $file # the three locations in the file should have been added + + # Get-RecentLocation doesn't include the current location, so three results + (Get-RecentLocation).Count | Should -Be 3 Remove-RecentLocation * - Test-Path TestDrive:/recent_dirs_2 | Should -Be $false - setocd RECENT_DIRS_FILE TestDrive:/recent_dirs_2 + cd TestDrive:/ + $file = Join-Path ([io.path]::GetTempPath()) ([io.path]::GetRandomFileName()) + Test-Path $file | Should -Be $false + setocd RECENT_DIRS_FILE $file Start-Sleep -Milliseconds 50 # save is async - Test-Path TestDrive:/recent_dirs_2 | Should -Be $true + Test-Path $file | Should -Be $true setocd RECENT_DIRS_FILE } diff --git a/tests/test.ps1 b/tests/test.ps1 index 6928186..a3d5c37 100644 --- a/tests/test.ps1 +++ b/tests/test.ps1 @@ -2,7 +2,7 @@ param ([switch] $Cover) if ($Cover) { $ex = "$PSScriptRoot/../cd-extras" - Invoke-Pester $PSScriptRoot\cd-extras.Tests.ps1 -CodeCoverage "$ex/public/*.ps1", "$ex/private/*-*.ps1" @args + Invoke-Pester $PSScriptRoot\cd-extras.Tests.ps1 -CodeCoverage "$ex/public/*-*.ps1", "$ex/private/*.ps1" @args } else { Invoke-Pester $PSScriptRoot\cd-extras.Tests.ps1 @args From ce3d2510bef17dcc53176286aed2ae333bf0be5e Mon Sep 17 00:00:00 2001 From: Nick Date: Wed, 22 Feb 2023 21:29:26 +1100 Subject: [PATCH 14/16] Bump version. --- cd-extras/cd-extras.psd1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cd-extras/cd-extras.psd1 b/cd-extras/cd-extras.psd1 index 78e6727..ed6d1be 100644 --- a/cd-extras/cd-extras.psd1 +++ b/cd-extras/cd-extras.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'cd-extras.psm1' - ModuleVersion = '2.9.4' + ModuleVersion = '3.0.0' GUID = '206fccbd-dc96-4b23-908c-5ac821372e16' Author = 'Nick Cox' @@ -15,7 +15,8 @@ PrivateData = @{ PSData = @{ - ReleaseNotes = 'Fix an issue where UNC paths not expanded properly.' + Prerelease = 'beta1' + ReleaseNotes = 'Adds Set-Recent and Set-Frecent functionality' Tags = @('cd+', 'cd-', 'AUTO_CD', 'CD_PATH', 'CDABLE_VARS', 'bash', 'zsh') LicenseUri = 'https://github.com/nickcox/cd-extras/blob/master/LICENSE' ProjectUri = 'https://github.com/nickcox/cd-extras' From ffda3d4c53014c29d74085f52cc019054b22b979 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 17 Mar 2026 20:49:49 +1100 Subject: [PATCH 15/16] Add swappable frecency provider and auto-detect zoxide at startup. --- cd-extras/public/Add-Bookmark.ps1 | 2 +- cd-extras/public/Get-FrecentLocation.ps1 | 4 +++- cd-extras/public/Set-FrecentLocation.ps1 | 10 +++++----- cd-extras/public/_Classes.ps1 | 7 +++++++ 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/cd-extras/public/Add-Bookmark.ps1 b/cd-extras/public/Add-Bookmark.ps1 index c3b276e..78ca164 100644 --- a/cd-extras/public/Add-Bookmark.ps1 +++ b/cd-extras/public/Add-Bookmark.ps1 @@ -31,5 +31,5 @@ function Add-Bookmark() { [Parameter(Position = 0, ValueFromPipeline)] [string] $Path = $PWD ) - Process { if (Test-Path $Path) { UpdateRecent (Resolve-Path $Path) $true } } + Process { if (Test-Path $Path) { UpdateRecent (Resolve-Path $Path).Path $true } } } diff --git a/cd-extras/public/Get-FrecentLocation.ps1 b/cd-extras/public/Get-FrecentLocation.ps1 index 4e26827..fcb25b3 100644 --- a/cd-extras/public/Get-FrecentLocation.ps1 +++ b/cd-extras/public/Get-FrecentLocation.ps1 @@ -56,7 +56,9 @@ function Get-FrecentLocation { [Parameter(ValueFromRemainingArguments)] [string[]] $Terms ) - $recents = @(GetFrecent $First $Terms) + $recents = if ($cde.FrecentProvider) { + &$cde.FrecentProvider @Terms | select -First $First + } else { @(GetFrecent $First $Terms) } if ($recents.Count) { IndexPaths $recents } } diff --git a/cd-extras/public/Set-FrecentLocation.ps1 b/cd-extras/public/Set-FrecentLocation.ps1 index fd2bfc6..e7c5908 100644 --- a/cd-extras/public/Set-FrecentLocation.ps1 +++ b/cd-extras/public/Set-FrecentLocation.ps1 @@ -14,7 +14,7 @@ List the matching frecent locations instead of changing directory. Equivalent to The current directory is always excluded from the list. .PARAMETER ListTerms -Terms to matching when listing frecent locations. This can be a single term or a comma or space separated list. +Terms to match when listing frecent locations. This can be a single term or a comma or space separated list. The last (or only) term must match the leaf name of a directory in order to be considered a match. .PARAMETER First @@ -67,7 +67,7 @@ Remove-Bookmark function Set-FrecentLocation { [OutputType([void])] - [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'n')] + [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'named')] param( [Parameter(ParameterSetName = 'n', Position = 0)] [uint16] $n = 1, @@ -106,13 +106,13 @@ function Set-FrecentLocation { [switch] $PassThru ) - if ($PSCmdlet.ParameterSetName -eq 'n' -and $n -ge 1) { - $recents = @(GetFrecent $n) + if (($PSCmdlet.ParameterSetName -eq 'n') -and $n -ge 1) { + $recents = @(Get-FrecentLocation -First $n) if ($recents.Count -ge $n) { Set-LocationEx $recents[$n - 1] -PassThru:$PassThru } } if ($PSCmdlet.ParameterSetName -eq 'named') { - $recents = @(GetFrecent 1 $Terms) + $recents = @(Get-FrecentLocation -First 1 $Terms) if ($recents) { Set-LocationEx $recents[0] -PassThru:$PassThru } elseif ($cde.RecentDirsFallThrough -and $Terms.Length -eq 1) { Set-LocationEx $Terms[0] -PassThru:$PassThru } else { Write-Error "Could not find '$Terms' in frecent locations." -ErrorAction Stop } diff --git a/cd-extras/public/_Classes.ps1 b/cd-extras/public/_Classes.ps1 index 3b6c785..4532a14 100644 --- a/cd-extras/public/_Classes.ps1 +++ b/cd-extras/public/_Classes.ps1 @@ -39,8 +39,15 @@ class CdeOptions { [bool] $IndexedCompletion = (Get-Module PSReadLine) -and ( Get-PSReadLineKeyHandler -Bound | Where Function -eq MenuComplete ) + [scriptblock] $FrecentProvider = $null [ScriptBlock] $ToolTip = { param ($item, $isTruncated) "{0} $(if ($isTruncated) {'{1}'})" -f $item, "$([char]27)[3m(+additional results not displayed)$([char]27)[0m" } + + CdeOptions() { + if ((Get-Command zoxide -ErrorAction Ignore) -is [System.Management.Automation.ApplicationInfo]) { + $this.FrecentProvider = { &zoxide query -l -- $args } + } + } } From b4bdddd4c6376dcdd0a2c3fac6d69f93801e84af Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 17 Mar 2026 22:53:51 +1100 Subject: [PATCH 16/16] Add publish workflow and cut beta2 --- .github/workflows/publish.yaml | 17 +++++++++++++++++ cd-extras/cd-extras.psd1 | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/publish.yaml diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..cf4f0ba --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,17 @@ +name: Publish to PowerShell Gallery + +on: workflow_dispatch + +jobs: + publish: + name: Publish to PSGallery + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Publish + env: + PSNugetKey: ${{ secrets.PSNugetKey }} + run: ./publishme.ps1 -Confirm:$false + shell: pwsh diff --git a/cd-extras/cd-extras.psd1 b/cd-extras/cd-extras.psd1 index ed6d1be..3f7d3c7 100644 --- a/cd-extras/cd-extras.psd1 +++ b/cd-extras/cd-extras.psd1 @@ -15,7 +15,7 @@ PrivateData = @{ PSData = @{ - Prerelease = 'beta1' + Prerelease = 'beta2' ReleaseNotes = 'Adds Set-Recent and Set-Frecent functionality' Tags = @('cd+', 'cd-', 'AUTO_CD', 'CD_PATH', 'CDABLE_VARS', 'bash', 'zsh') LicenseUri = 'https://github.com/nickcox/cd-extras/blob/master/LICENSE'