├── .gitattributes ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── ZLocation.Tests ├── Meta.Tests.ps1 ├── ZLocation.Search.Tests.ps1 ├── ZLocation.Storage.Tests.ps1 ├── ZLocation.Tests.ps1 └── _mocks.ps1 ├── ZLocation ├── LiteDB │ ├── LICENSE │ └── LiteDB.dll ├── ZLocation.LiteDB.psd1 ├── ZLocation.LiteDB.psm1 ├── ZLocation.Search.psm1 ├── ZLocation.Service.psd1 ├── ZLocation.Service.psm1 ├── ZLocation.Storage.psm1 ├── ZLocation.psd1 └── ZLocation.psm1 └── appveyor.yml /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ignored -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ------------- 3 | ## 1.4.3 4 | * README and setup instruction improvements ([#106](https://github.com/vors/ZLocation/pull/106)) (Thanks @deltoss) 5 | * Avoid an exception processing results in Get-ZLocation ([#112](https://github.com/vors/ZLocation/issues/112)) (Thanks @kahlin) 6 | 7 | ## 1.4.2 8 | * Stop PowerShell < 6 on Windows emitting an error on import. ([#103](https://github.com/vors/ZLocation/issues/103)) 9 | 10 | ## 1.4.1 11 | * Resolve an OS detection bug resulting in errors when importing module on macOS ([#100](https://github.com/vors/ZLocation/issues/100)) (Thanks @nheath!) 12 | 13 | ## 1.4.0 14 | * Attempt to handle the malformed database entries causing a number of reported issues ([#87](https://github.com/vors/ZLocation/pull/87)) 15 | * Add locations from Windows 10's Frequent Folders list to database ([#95](https://github.com/vors/ZLocation/pull/95)) 16 | 17 | ## 1.3.0 18 | * Prefer exact match over the weight when picking the location (#90) 19 | 20 | ## 1.2.0 21 | 22 | * Move to unvisited directories if one is provided (#80) 23 | * Fix - using ZLocation on Windows PowerShell creates errors in $Error (#75) 24 | 25 | ## 1.1.0 26 | 27 | * Fix problems with non-unified casing #62 (thanks @cspotcode) 28 | * Better representation for db entries (thanks @rkeithhill) 29 | * Graceful install/remove for the prompt hook (thanks @rkeithhill) 30 | * Add retry and backoff logic to better support multiply db connections on Mac. 31 | 32 | ## 1.0.0 33 | 34 | * Make ZLocation work with `cmder` 35 | * Replace persistent storage by LiteDB (thanks @cspotcode) 36 | * Make ZLocation work on Linux and MacOS (thanks @cspotcode) 37 | * Prioritize the beginning of a foldername in ranking 38 | * Use `Register-ArgumentCompleter` for tab-completions (thanks @cspotcode) 39 | * New shortcuts `z` and `z -l` for quick quering 40 | 41 | ## 0.3.0 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Sergei Vorobev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build status](https://ci.appveyor.com/api/projects/status/qqg75o50jj6e35mn/branch/master?svg=true)](https://ci.appveyor.com/project/vors/zlocation/branch/master) 2 | 3 | ZLocation 4 | ========= 5 | 6 | [![Join the chat at https://gitter.im/vors/ZLocation](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/vors/ZLocation?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 7 | 8 | Tracks your most used directories, based on number of previously run commands. 9 | After a short learning phase, `z` will take you to the most popular directory that matches all of the regular expressions given on the command line. 10 | You can use **Tab-Completion / Intellisense** to pick directories that are not the first choice. 11 | 12 | ZLocation is the successor of [Jump-Location](https://github.com/tkellogg/Jump-Location). 13 | Like [z.sh](https://github.com/rupa/z) is a reimagined clone of [autojump](https://github.com/joelthelion/autojump), Zlocation is a reimagined clone of Jump-Location. 14 | 15 | Usage 16 | ----- 17 | 18 | ZLocation keeps track of your `$pwd` (current folder). 19 | Once visited, folder become known to ZLocation. 20 | You can `cd` with just a hint of the path! 21 | 22 | The full command name is `Invoke-ZLocation`, but in examples I use alias `z`. 23 | It's all about navigation speed, isn't it? 24 | 25 | ``` 26 | PS C:\Users\sevoroby> z c: 27 | PS C:\> z zlo 28 | PS C:\dev\ZLocation> z dsc 29 | PS C:\dev\azure-sdk-tools\src\ServiceManagement\Compute\Commands.ServiceManagement\IaaS\Extensions\DSC> z test 30 | PS C:\dev\ZLocation\ZLocation.Tests> 31 | ``` 32 | 33 | ### List known locations 34 | 35 | `z` without arguments will list all the known locations and their weights (short-cut for `Get-ZLocation`) 36 | 37 | To see all locations matched to a query `foo` use `z -l foo`. 38 | 39 | ### Navigating to less common directories with tab completion 40 | 41 | If `z mydir` doesn't take you to the correct directory, you can also tab through 42 | ZLocation's suggestions. 43 | 44 | For example, pressing tab with `z src` will take you through all of ZLocation's 45 | completions for `src`. 46 | 47 | ### Going back 48 | 49 | ZLocation keeps a stack of directories as you jump between them. `z -` will 50 | "pop" the stack: it will move you to the previous directory you jumped to, 51 | basically letting you undo your `z` navigation. 52 | 53 | If the stack is empty (you have only jumped once), `z -` will take you to your 54 | original directory. 55 | 56 | For example: 57 | 58 | ```ps 59 | C:\>z foo 60 | C:\foo>z bar 61 | C:\baz\bar> z - 62 | C:\foo>z - 63 | C:\>z - 64 | C:\>#no-op 65 | ``` 66 | 67 | Goals / Key features 68 | -------------------- 69 | 70 | * Support for multiple PS sessions. 71 | * Good built-in ranking algorithm. 72 | * ~~Customizable matching algorithm and weight function.~~ 73 | * Works on Windows, Linux and MacOS. 74 | 75 | ## Install 76 | Install from [PowerShellGet Gallery](https://www.powershellgallery.com/packages/ZLocation/) 77 | ```powershell 78 | Install-Module ZLocation -Scope CurrentUser 79 | ``` 80 | 81 | Make sure to **include ZLocation import in your `$PROFILE`**. 82 | It intentionally doesn't alter `$PROFILE` automatically on installation. 83 | 84 | This one-liner installs ZLocation, imports it and adds it to a profile. 85 | 86 | ```powershell 87 | Install-Module ZLocation -Scope CurrentUser; Import-Module ZLocation; Add-Content -Value "`r`n`r`nImport-Module ZLocation`r`n" -Encoding utf8 -Path $PROFILE.CurrentUserAllHosts 88 | ``` 89 | 90 | If you want to display some additional information about ZLocation on start-up, you can put this snippet in `$PROFILE` after import. 91 | ```powershell 92 | Write-Host -Foreground Green "`n[ZLocation] knows about $((Get-ZLocation).Keys.Count) locations.`n" 93 | ``` 94 | 95 | ### Note 96 | 97 | ZLocation alternates your prompt function to track the location. Meaning if you use this module with other modules that modifies your prompt function (e.g. such as `posh-git`), then you'd need to adjust your [Powershell profile file](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_profiles?view=powershell-7). The statement `Import-Module ZLocation` needs to be placed **after** the other module imports that modifies your prompt function. 98 | 99 | You can open up `profile.ps1` through using any of the below commands: 100 | 101 | ```powershell 102 | notepad $PROFILE.CurrentUserAllHosts 103 | notepad $env:USERPROFILE\Documents\WindowsPowerShell\profile.ps1 104 | notepad $Home\Documents\WindowsPowerShell\profile.ps1 105 | ``` 106 | 107 | Alternatively, type up the below in your file explorer, and then edit the `profile.ps1` file with an editor of your choice: 108 | 109 | ``` 110 | %USERPROFILE%\Documents\WindowsPowerShell 111 | ``` 112 | 113 | License 114 | ------- 115 | 116 | ZLocation is released under the [MIT](LICENSE) license. 117 | 118 | ZLocation bundles a copy of [LiteDB](http://www.litedb.org/). 119 | 120 | ### LiteDB License 121 | 122 | [MIT](http://opensource.org/licenses/MIT) 123 | 124 | Copyright (c) 2017 - Maurício David. 125 | 126 | Develop 127 | ------- 128 | 129 | ### Run tests 130 | 131 | Install [Pester](https://github.com/pester/Pester). 132 | Run `Invoke-Pester` from the root folder. 133 | -------------------------------------------------------------------------------- /ZLocation.Tests/Meta.Tests.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = 'stop' 2 | Set-StrictMode -Version latest 3 | 4 | $RepoRoot = (Resolve-Path $PSScriptRoot\..).Path 5 | 6 | Describe 'Text files formatting' { 7 | 8 | $allTextFiles = Get-ChildItem $RepoRoot -Exclude ignored | % { 9 | Get-ChildItem -file -recurse $_ -Exclude *.dll 10 | } 11 | 12 | Context 'Files encoding' { 13 | 14 | It "Doesn't use Unicode encoding" { 15 | $allTextFiles | %{ 16 | $path = $_.FullName 17 | $bytes = [System.IO.File]::ReadAllBytes($path) 18 | $zeroBytes = @($bytes -eq 0) 19 | if ($zeroBytes.Length) { 20 | Write-Warning "File $($_.FullName) contains 0 bytes. It's probably uses Unicode and need to be converted to UTF-8" 21 | } 22 | $zeroBytes.Length | Should Be 0 23 | } 24 | } 25 | } 26 | 27 | Context 'Indentations' { 28 | 29 | It "We are using spaces for indentaion, not tabs" { 30 | $totalTabsCount = 0 31 | $allTextFiles | %{ 32 | $fileName = $_.FullName 33 | Get-Content $_.FullName -Raw | Select-String "`t" | % { 34 | Write-Warning "There are tab in $fileName" 35 | $totalTabsCount++ 36 | } 37 | } 38 | $totalTabsCount | Should Be 0 39 | } 40 | } 41 | } 42 | 43 | Describe 'Version consistency' { 44 | 45 | It 'uses consistent version in ZLocation.psd1 and appveyor.yml' { 46 | # TODO: can we use some yml parser for that? 47 | $ymlVersionLine = Get-Content $RepoRoot\appveyor.yml | ? {$_ -like 'version: *'} | Select -first 1 48 | # i.e. $ymlVersionLine = 'version: 1.7.0.{build}' 49 | $ymlVersionLine | Should Not BeNullOrEmpty 50 | 51 | $manifest = (Get-Content $RepoRoot\ZLocation\ZLocation.psd1 -Raw) | iex 52 | "version: $($manifest.ModuleVersion).{build}" | Should be $ymlVersionLine 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /ZLocation.Tests/ZLocation.Search.Tests.ps1: -------------------------------------------------------------------------------- 1 | Import-Module $PSScriptRoot\..\ZLocation\ZLocation.Search.psm1 -Force 2 | 3 | if($IsWindows -eq $null) { 4 | $IsWindows = $true 5 | } 6 | 7 | Describe 'Find-Matches filters results correctly' { 8 | Context 'Equal weight' { 9 | if($IsWindows) { 10 | $data = @{ 11 | 'C:\foo1\foo2\foo3' = 1.0 12 | 'C:\foo1\foo2' = 1.0 13 | 'C:\foo1' = 1.0 14 | 'C:\' = 1.0 15 | } 16 | $rootPath = 'C:\' 17 | $foo1Path = 'C:\foo1' 18 | $foo2Path = 'C:\foo1\foo2' 19 | $foo3Path = 'C:\foo1\foo2\foo3' 20 | $pathSep = '\' 21 | } else { 22 | $data = @{ 23 | '/foo1/foo2/foo3' = 1.0 24 | '/foo1/foo2' = 1.0 25 | '/foo1' = 1.0 26 | '/' = 1.0 27 | } 28 | $rootPath = '/' 29 | $foo1Path = '/foo1' 30 | $foo2Path = '/foo1/foo2' 31 | $foo3Path = '/foo1/foo2/foo3' 32 | $pathSep = '/' 33 | } 34 | 35 | It 'Does not modify data' { 36 | Find-Matches $data fuuuu 37 | $data.Count | Should be 4 38 | } 39 | 40 | It 'returns only leave result' { 41 | Find-Matches $data foo2 | Should Be $foo2Path 42 | } 43 | 44 | It 'returns multiply results' { 45 | (Find-Matches $data foo | measure).Count | Should Be 3 46 | } 47 | 48 | It 'should be case-insensitive' { 49 | Find-Matches $data FoO1 | Should Be $foo1Path 50 | } 51 | 52 | If($IsWindows) { 53 | It 'returns disk root folder for C:' { 54 | Find-Matches $data C: | Should Be $rootPath 55 | } 56 | 57 | It 'returns disk root folder for C' { 58 | Find-Matches $data C | Should Be $rootPath 59 | } 60 | } else { 61 | It 'returns disk root folder for /' { 62 | Find-Matches $data / | Should Be $rootPath 63 | } 64 | } 65 | 66 | It "should ignore trailing $pathSep" { 67 | Find-Matches $data "$foo1Path$pathSep" | Should Be $foo1Path 68 | } 69 | 70 | } 71 | 72 | Context 'Different weight' { 73 | if($IsWindows) { 74 | $data = @{ 75 | 'C:\admin' = 1.0 76 | 'C:\admin\monad' = 2.0 77 | } 78 | $adminPath = 'C:\admin' 79 | } else { 80 | $data = @{ 81 | '/admin' = 1.0 82 | '/admin/monad' = 2.0 83 | } 84 | $adminPath = '/admin' 85 | } 86 | 87 | It 'Uses leaf match' { 88 | Find-Matches $data 'adm' | Should Be $adminPath 89 | } 90 | } 91 | 92 | Context 'Prefer prefix over weight' { 93 | if($IsWindows) { 94 | $fooPath = 'C:\foo' 95 | $afooPath = 'C:\afoo' 96 | } else { 97 | $fooPath = '/foo' 98 | $afooPath = '/afoo' 99 | } 100 | $data = @{ 101 | $fooPath = 1.0 102 | $afooPath = 1000.0 103 | } 104 | 105 | It 'Uses prefix match' { 106 | Find-Matches $data 'fo' | Should Be @($fooPath, $afooPath) 107 | } 108 | } 109 | 110 | Context 'Prefer exact match over weight and prefix' { 111 | if($IsWindows) { 112 | $fooPath = 'C:\foo' 113 | $afooPath = 'C:\foo2' 114 | } else { 115 | $fooPath = '/foo' 116 | $afooPath = '/foo2' 117 | } 118 | $data = @{ 119 | $fooPath = 1.0 120 | $afooPath = 1000.0 121 | } 122 | 123 | It 'Uses prefix match' { 124 | Find-Matches $data 'foo' | Should Be @($fooPath, $afooPath) 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /ZLocation.Tests/ZLocation.Storage.Tests.ps1: -------------------------------------------------------------------------------- 1 | Import-Module $PSScriptRoot\..\ZLocation\ZLocation.Storage.psm1 -Force 2 | 3 | . "$PSScriptRoot/_mocks.ps1" 4 | 5 | Describe 'ZLocation.Storage' { 6 | 7 | $originalCount = (Get-ZLocation).Count 8 | 9 | try { 10 | 11 | $path = [guid]::NewGuid().Guid 12 | $path2 = [guid]::NewGuid().Guid 13 | $path3 = 'FOO' 14 | $path4 = 'foo' 15 | 16 | It 'can add weight' { 17 | $w1 = 6.6 18 | $w2 = 10.0 19 | Add-ZWeight -path $path -weight $w1 20 | $h = Get-ZLocation 21 | $h.Count | Should Be ($originalCount + 1) 22 | $h[$path] | Should Be $w1 23 | 24 | Add-ZWeight -path $path -weight $w2 25 | $h = Get-ZLocation 26 | $h.Count | Should Be ($originalCount + 1) 27 | $h[$path] | Should Be ($w1 + $w2) 28 | } 29 | 30 | It 'can add another path' { 31 | Add-ZWeight -path $path2 -weight 1.0 32 | $h = Get-ZLocation 33 | $h.Count | Should Be ($originalCount + 2) 34 | } 35 | 36 | It 'can filter paths' { 37 | $h = Get-ZLocation -Match $path2.Substring(0,5) 38 | $path | Should -Not -BeIn $h.Keys # Fails in Pester 3 - no BeIn 39 | $path2 | Should -BeIn $h.Keys # Fails in Pester 3 - no BeIn 40 | } 41 | 42 | It 'can filter paths with multiple filters' { 43 | $h = Get-ZLocation -Match $path2.Substring(0,5),$path.Substring(0,5) 44 | $path | Should -BeIn $h.Keys # Fails in Pester 3 - no BeIn 45 | $path2 | Should -BeIn $h.Keys # Fails in Pester 3 - no BeIn 46 | } 47 | 48 | It 'can handle multiple paths differing only by capitalization' { 49 | Add-ZWeight -path $path3 -weight 1 50 | Add-ZWeight -path $path4 -weight 1 51 | Get-ZLocation 52 | } 53 | 54 | } finally { 55 | 56 | It 'can remove path' { 57 | Remove-ZLocation -path $path2 58 | Remove-ZLocation -path $path3 59 | Remove-ZLocation -path $path4 60 | $h = Get-ZLocation 61 | $h.Count | Should Be ($originalCount + 1) 62 | 63 | Remove-ZLocation -path $path 64 | $h = Get-ZLocation 65 | $h.Count | Should Be $originalCount 66 | } 67 | 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /ZLocation.Tests/ZLocation.Tests.ps1: -------------------------------------------------------------------------------- 1 | # Integration tests. 2 | 3 | # Maybe -Force re-import nested modules too? 4 | #Import-Module $PSScriptRoot\..\ZLocation\ZLocation.Storage.psm1 -Force 5 | #Import-Module $PSScriptRoot\..\ZLocation\ZLocation.Search.psm1 -Force 6 | Import-Module $PSScriptRoot\..\ZLocation\ZLocation.psd1 -Force 7 | 8 | . "$PSScriptRoot/_mocks.ps1" 9 | 10 | Describe 'ZLocation' { 11 | 12 | Context 'Success scenario' { 13 | 14 | It 'can execute scenario with new directory' { 15 | try { 16 | $newdirectory = [guid]::NewGuid().Guid 17 | $curDirFullPath = ($pwd).Path 18 | mkdir $newdirectory 19 | cd $newdirectory 20 | $newDirFullPath = ($pwd).Path 21 | # trigger weight update 22 | prompt > $null 23 | # go back 24 | cd $curDirFullPath 25 | 26 | # do the jump 27 | z ($newdirectory.Substring(0, 3)) 28 | ($pwd).Path | Should Be $newDirFullPath 29 | 30 | # verify that pop-location can be used after z 31 | z - 32 | ($pwd).Path | Should Be $curDirFullPath 33 | 34 | $h = Get-ZLocation 35 | $h[$newDirFullPath] | Should Be 1 36 | } finally { 37 | cd $curDirFullPath 38 | Remove-Item -rec -force $newdirectory 39 | Remove-ZLocation $newDirFullPath 40 | } 41 | } 42 | 43 | It 'can navigate to an unvisited directory' { 44 | try { 45 | $newdirectory = [guid]::NewGuid().Guid 46 | $curDirFullPath = ($pwd).Path 47 | mkdir $newdirectory 48 | $newDirFullPath = Join-Path $curDirFullPath $newdirectory 49 | 50 | # do the jump 51 | z $newdirectory 52 | ($pwd).Path | Should Be $newDirFullPath 53 | } 54 | finally { 55 | cd $curDirFullPath 56 | Remove-Item -rec -force $newdirectory 57 | Remove-ZLocation $newDirFullPath 58 | } 59 | } 60 | } 61 | 62 | Context 'tab completion' { 63 | Function Complete($command) { 64 | [System.Management.Automation.CommandCompletion]::CompleteInput($command, $command.Length, @{}).CompletionMatches.ListItemText 65 | (TabExpansion2 $command $command.Length).CompletionMatches.ListItemText 66 | } 67 | BeforeEach { 68 | Update-ZLocation 'prefixABC' 69 | Update-ZLocation 'prefixDEF' 70 | Update-ZLocation 'notthisGHI' 71 | Update-ZLocation 'notthisJKL' 72 | } 73 | AfterEach { 74 | Remove-ZLocation -path 'prefixABC' 75 | Remove-ZLocation -path 'prefixDEF' 76 | Remove-ZLocation -path 'notthisGHI' 77 | Remove-ZLocation -path 'notthisJKL' 78 | } 79 | It 'should offer only completions matching entered text' { 80 | $completions = Complete 'z refix' 81 | $completions | Should -Contain 'prefixABC' 82 | $completions | Should -Contain 'prefixDEF' 83 | $completions | Should -Not -Contain 'notthisGHI' 84 | $completions | Should -Not -Contain 'notthisJKL' 85 | } 86 | It 'should offer all completions when invoked without prefix' { 87 | $completions = Complete 'z ' 88 | $completions | Should -Contain 'prefixABC' 89 | $completions | Should -Contain 'prefixDEF' 90 | $completions | Should -Contain 'notthisGHI' 91 | $completions | Should -Contain 'notthisJKL' 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /ZLocation.Tests/_mocks.ps1: -------------------------------------------------------------------------------- 1 | # Use a testing database; do not touch user's primary database 2 | $testDb = Join-Path $PSScriptRoot '../ignored/z-location-tests.db' 3 | $legacyBackup = Join-Path $PSScriptRoot '../ignored/z-location-tests-legacy-backup.txt' 4 | if(-not (Test-Path (Split-Path -Parent $testdb))) { New-Item -Type Directory (Split-Path -Parent $testdb) } 5 | if(Get-Module -all ZLocation.Service) { 6 | Mock -ModuleName ZLocation.Service Get-ZLocationDatabaseFilePath { 7 | $testDb 8 | }.getNewClosure() -ErrorAction SilentlyContinue 9 | Mock -ModuleName ZLocation.Service Get-ZLocationLegacyBackupFilePath { 10 | $testDb 11 | }.getNewClosure() -ErrorAction SilentlyContinue 12 | } 13 | -------------------------------------------------------------------------------- /ZLocation/LiteDB/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2015 Mauricio David 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ZLocation/LiteDB/LiteDB.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vors/ZLocation/cdd940b41f2f4ed638932716bf336bd22d0f9328/ZLocation/LiteDB/LiteDB.dll -------------------------------------------------------------------------------- /ZLocation/ZLocation.LiteDB.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | ModuleVersion = '1.0' 3 | GUID = 'c9e37f57-8308-441b-adb9-fb98f6bbc6c9' 4 | RequiredAssemblies = @("$PSScriptRoot/LiteDB/LiteDB.dll") 5 | RootModule = 'ZLocation.LiteDB.psm1' 6 | FunctionsToExport = '*' 7 | CmdletsToExport = @() 8 | VariablesToExport = @() 9 | AliasesToExport = @() 10 | PrivateData = @{ 11 | PSData = @{} 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /ZLocation/ZLocation.LiteDB.psm1: -------------------------------------------------------------------------------- 1 | $mapper = [LiteDB.BSONMapper]::new() 2 | 3 | Function Open([string]$connectionString) { 4 | [LiteDB.LiteDatabase]::new($connectionString) 5 | } 6 | 7 | Function Get-Collection($db, $name) { 8 | $db.GetCollection($name) 9 | } 10 | 11 | Function ToDocument($obj) { 12 | ,$mapper.ToDocument($obj) 13 | } 14 | Function ToObject($type, $obj) { 15 | ,$mapper.ToObject($type, $obj) 16 | } 17 | 18 | function Insert([LiteDB.LiteCollection[LiteDB.BSONDocument]]$collection, $item) { 19 | $collection.Insert((ToDocument $item)) | out-null 20 | } 21 | 22 | function Update([LiteDB.LiteCollection[LiteDB.BSONDocument]]$collection, $item) { 23 | $collection.Update((ToDocument $item)) 24 | } 25 | 26 | function Delete([LiteDB.LiteCollection[LiteDB.BSONDocument]]$collection, $query) { 27 | $collection.Delete($query) 28 | } 29 | 30 | Function GetById([LiteDB.LiteCollection[LiteDB.BSONDocument]]$collection, $id, $type) { 31 | Find $collection ([LiteDB.Query]::EQ('_id', [LiteDB.BSONValue]::new($id))) $type 32 | } 33 | 34 | Function Find([LiteDB.LiteCollection[LiteDB.BSONDocument]]$collection, [LiteDB.Query]$query, $type) { 35 | ForEach($document in $collection.Find([LiteDB.Query]$query)) { 36 | ToObject $type $document 37 | } 38 | } 39 | 40 | Function CreateMatchQuery($matches) { 41 | $query = [LiteDB.Query]::All() 42 | ForEach($prop in (getEnum $matches)) { 43 | $query = [LiteDB.Query]::And([LiteDB.Query]::EQ($prop.Name, $prop.Value), $query) 44 | } 45 | ,$query 46 | } 47 | -------------------------------------------------------------------------------- /ZLocation/ZLocation.Search.psm1: -------------------------------------------------------------------------------- 1 | Set-StrictMode -Version Latest 2 | 3 | if ((Get-Variable IsWindows -ErrorAction Ignore) -eq $null) { $IsWindows = $true } 4 | 5 | function Find-Matches { 6 | param ( 7 | [Parameter(Mandatory=$true)] [hashtable]$hash, 8 | [string[]]$query 9 | ) 10 | $hash = $hash.Clone() 11 | foreach ($key in ($hash.GetEnumerator() | %{$_.Name})) 12 | { 13 | if (-not (Test-FuzzyMatch $key $query)) 14 | { 15 | $hash.Remove($key) 16 | } 17 | } 18 | 19 | if ($query -ne $null -and $query.Length -gt 0) { 20 | $lowerPrefix = $query[-1].ToLower() 21 | # we should prefer paths that start with the query over paths with bigger weight 22 | # that don't. 23 | # i.e. if we have 24 | # /foo = 1.0 25 | # /afoo = 2.0 26 | # and query is "fo", we should prefer /foo 27 | # Similarly, with the same query `fo`, the full match `/fo` should win over `/fo2` 28 | $res = $hash.GetEnumerator() | % { 29 | New-Object -TypeName PSCustomObject -Property @{ 30 | Name=$_.Name 31 | Value=$_.Value 32 | Starts=[int](Start-WithPrefix -Path $_.Name -lowerPrefix $lowerPrefix) 33 | IsExactMatch=[int](IsExactMatch -Path $_.Name -lowerPrefix $lowerPrefix) 34 | } 35 | } | Sort-Object -Property IsExactMatch, Starts, Value -Descending 36 | } else { 37 | $res = $hash.GetEnumerator() | Sort-Object -Property Value -Descending 38 | } 39 | 40 | if ($res) { 41 | $res | %{$_.Name} 42 | } 43 | } 44 | 45 | function Start-WithPrefix { 46 | param ( 47 | [Parameter(Mandatory=$true)] [string]$Path, 48 | [Parameter(Mandatory=$true)] [string]$lowerPrefix 49 | ) 50 | $lowerLeaf = (Split-Path -Leaf $Path).ToLower() 51 | return $lowerLeaf.StartsWith($lowerPrefix) 52 | } 53 | 54 | function IsExactMatch { 55 | param ( 56 | [Parameter(Mandatory=$true)] [string]$Path, 57 | [Parameter(Mandatory=$true)] [string]$lowerPrefix 58 | ) 59 | $lowerLeaf = (Split-Path -Leaf $Path).ToLower() 60 | return $lowerLeaf -eq $lowerPrefix 61 | } 62 | 63 | function Test-FuzzyMatch { 64 | param ( 65 | [Parameter(Mandatory=$true)] [string]$path, 66 | [string[]]$query 67 | ) 68 | function contains([string]$left, [string]$right) { 69 | return [bool]($left.IndexOf($right, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) 70 | } 71 | 72 | if ($query -eq $null) { 73 | return $true 74 | } 75 | $n = $query.Length 76 | if ($n -eq 0) 77 | { 78 | # empty query match to everything 79 | return $true 80 | } 81 | 82 | for ($i=0; $i -lt $n-1; $i++) 83 | { 84 | if (-not (contains -left $path -right $query[$i])) 85 | { 86 | return $false 87 | } 88 | } 89 | 90 | # after tab expansion, we get desired full path as a last query element. 91 | # tab expansion can come from our code, then it will represent the full path. 92 | # It also can come from the standard tab expansion (when our doesn't return anything), which is file system based. 93 | # It can produce relative paths. 94 | 95 | $rootQuery = $query[$n-1] 96 | 97 | if (-not [System.IO.Path]::IsPathRooted($rootQuery)) { 98 | # handle '..\foo' case 99 | $rootQueryCandidate = Join-Path $pwd $query[$n-1] 100 | if (Test-Path $rootQueryCandidate) { 101 | $rootQuery = (Resolve-Path $rootQueryCandidate).Path 102 | } 103 | } 104 | 105 | if ([System.IO.Path]::IsPathRooted($rootQuery)) 106 | { 107 | # doing a tweak to handle 'C:' and 'C:\' cases correctly. 108 | if ($IsWindows -and ($rootQuery.Length) -eq 2 -and ($rootQuery[-1] -eq ':')) 109 | { 110 | $rootQuery = $rootQuery + "\" 111 | } 112 | # doing a tweaks to handle 'C:\foo' and 'C:\foo\' cases correctly. 113 | elseif ($rootQuery -ne '/' -and $rootQuery[-1] -eq [IO.Path]::DirectorySeparatorChar) 114 | { 115 | $rootQuery = $rootQuery.Substring(0, $rootQuery.Length-1) 116 | } 117 | return $path -eq $rootQuery 118 | } 119 | 120 | $leaf = Split-Path -Leaf $path 121 | return (contains -left $leaf -right $query[$n-1]) 122 | } 123 | 124 | Export-ModuleMember -Function Find-Matches 125 | -------------------------------------------------------------------------------- /ZLocation/ZLocation.Service.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | ModuleVersion = '1.0' 3 | GUID = '3d256bab-55d1-459c-8673-1d9d7ca8554a' 4 | # Assembly must be loaded first or else powershell class will fail to compile 5 | RequiredAssemblies = @("$PSScriptRoot/LiteDB/LiteDB.dll") 6 | RootModule = 'ZLocation.Service.psm1' 7 | FunctionsToExport = @('Get-ZService') 8 | CmdletsToExport = @() 9 | VariablesToExport = @() 10 | AliasesToExport = @() 11 | PrivateData = @{ 12 | PSData = @{} 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /ZLocation/ZLocation.Service.psm1: -------------------------------------------------------------------------------- 1 | Set-StrictMode -Version Latest 2 | 3 | Import-Module -Prefix DB (Join-Path $PSScriptRoot 'ZLocation.LiteDB.psd1') 4 | 5 | class Service { 6 | [Collections.Generic.IEnumerable[Location]] Get() { 7 | return (dboperation { 8 | # Return an enumerator of all location entries 9 | try { 10 | [Location[]]$arr = DBFind $collection ([LiteDB.Query]::All()) ([Location]) 11 | , $arr 12 | } 13 | catch [System.InvalidCastException] { 14 | Write-Warning "Caught InvalidCastException when reading db, probably [LiteDB.ObjectId] entry present." 15 | $oidquery = [LiteDB.Query]::Where('_id', {$args -like '{"$oid":"*"}'}) 16 | $problementries = (,$collection.Find($oidquery)) 17 | if ($problementries.Count -gt 0) { 18 | Write-Warning "Found $($problementries.Count) problem entries, attempting to remove..." 19 | $problementries | Write-Debug 20 | try { 21 | DBDelete $collection $oidquery | Out-Null 22 | Write-Warning 'Problem entries successfully removed, please repeat your command.' 23 | } catch { 24 | Write-Error 'Problem entries could not be removed.' 25 | } 26 | } else { 27 | Write-Error 'No problem entries found, please open an issue on https://github.com/vors/ZLocation' 28 | } 29 | } 30 | }) 31 | } 32 | [void] Add([string]$path, [double]$weight) { 33 | dboperation { 34 | $l = DBGetById $collection $path ([Location]) 35 | if($l) { 36 | $l.weight += $weight 37 | DBUpdate $collection $l 38 | } else { 39 | $l = [Location]::new() 40 | $l.path = $path 41 | $l.weight = $weight 42 | DBInsert $collection $l 43 | } 44 | } 45 | } 46 | [void] Remove([string]$path) { 47 | dboperation { 48 | # Use DB's internal column name, not mapped name 49 | DBDelete $collection ([LiteDB.Query]::EQ('_id', [LiteDB.BSONValue]::new($path))) 50 | } 51 | } 52 | } 53 | 54 | class Location { 55 | [LiteDB.BsonId()] 56 | [string] $path; 57 | 58 | [double] $weight; 59 | } 60 | 61 | function Get-ZLocationDatabaseFilePath 62 | { 63 | return (Join-Path $HOME 'z-location.db') 64 | } 65 | # Returns path to legacy ZLocation backup file. 66 | function Get-ZLocationLegacyBackupFilePath 67 | { 68 | if($env:USERPROFILE -ne $null) { 69 | Join-Path $env:USERPROFILE 'z-location.txt' 70 | } 71 | } 72 | 73 | <# 74 | Open database, invoke a database operation, and close the database afterwards. 75 | This is necessary for safe multi-process concurrency. 76 | See: https://github.com/mbdavid/LiteDB/wiki/Concurrency 77 | Exposes $db and $collection variables for use by the $scriptblock 78 | #> 79 | function dboperation { 80 | param ( 81 | [Parameter(Mandatory=$true)] $private:scriptblock 82 | ) 83 | $Private:Mode = if (Get-Variable IsMacOS -ErrorAction Ignore) { 'Exclusive' } else { 'Shared' } 84 | # $db and $collection will be in-scope within $scriptblock 85 | $db = DBOpen "Filename=$( Get-ZLocationDatabaseFilePath ); Mode=$Mode" 86 | $collection = Get-DBCollection $db 'location' 87 | try { 88 | # retry logic: on Mac we may not be able to execute the read concurrently 89 | for ($__i=0; $__i -lt 5; $__i++) { 90 | try { 91 | & $private:scriptblock 92 | return 93 | } catch [System.IO.IOException] { 94 | # The process cannot access the file '~\z-location.db' because it is being used by another process. 95 | if ($__i -lt 4 ) { 96 | $rand = Get-Random 100 97 | Start-Sleep -Milliseconds (($__i + 1) * 100 - $rand) 98 | } else { 99 | throw [System.IO.IOException] 'Cannot execute database operation after 5 attempts, please open an issue on https://github.com/vors/ZLocation' 100 | } 101 | } 102 | } 103 | } finally { 104 | $db.dispose() 105 | } 106 | } 107 | 108 | $dbExists = Test-Path (Get-ZLocationDatabaseFilePath) 109 | $legacyBackupPath = Get-ZLocationLegacyBackupFilePath 110 | $legacyBackupExists = ($legacyBackupPath -ne $null) -and (Test-Path $legacyBackupPath) 111 | 112 | # Create empty db, collection, and index if it doesn't exist 113 | dboperation { 114 | $collection.EnsureIndex('path') 115 | } 116 | 117 | $service = [Service]::new() 118 | 119 | # Migrate legacy backup into database if appropriate 120 | if((-not $dbExists) -and $legacyBackupExists) { 121 | Write-Warning "ZLocation changed storage from $legacyBackupPath to $(Get-ZLocationDatabaseFilePath), feel free to remove the old txt file" 122 | Get-Content $legacyBackupPath | Where-Object { $_ -ne $null } | ForEach-Object { 123 | $split = $_ -split "`t" 124 | $service.add($split[0], $split[1]) 125 | } 126 | } 127 | 128 | Function Get-ZService { 129 | ,$service 130 | } 131 | -------------------------------------------------------------------------------- /ZLocation/ZLocation.Storage.psm1: -------------------------------------------------------------------------------- 1 | Set-StrictMode -Version Latest 2 | 3 | Import-Module (Join-Path $PSScriptRoot 'ZLocation.Service.psd1') 4 | Import-Module (Join-Path $PSScriptRoot 'ZLocation.Search.psm1') 5 | 6 | function Get-ZLocation($Match) 7 | { 8 | $service = Get-ZService 9 | $hash = [Collections.HashTable]::new() 10 | foreach ($item in $service.Get()) 11 | { 12 | $hash[$item.path] = $item.weight 13 | } 14 | 15 | if ($Match) 16 | { 17 | # Create a new hash containing only matching locations 18 | $newhash = @{} 19 | $Match | %{Find-Matches $hash $_} | %{$newhash.add($_, $hash[$_])} 20 | $hash = $newhash 21 | } 22 | 23 | return $hash 24 | } 25 | 26 | function Add-ZWeight { 27 | param ( 28 | [Parameter(Mandatory=$true)] [string]$Path, 29 | [Parameter(Mandatory=$true)] [double]$Weight 30 | ) 31 | $service = Get-ZService 32 | $service.Add($path, $weight) 33 | } 34 | 35 | function Remove-ZLocation { 36 | param ( 37 | [Parameter(Mandatory=$true)] [string]$Path 38 | ) 39 | $service = Get-ZService 40 | $service.Remove($path) 41 | } 42 | 43 | $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { 44 | Write-Warning "[ZLocation] module was removed, but service was not closed." 45 | } 46 | 47 | Export-ModuleMember -Function @("Get-ZLocation", "Add-ZWeight", "Remove-ZLocation") 48 | -------------------------------------------------------------------------------- /ZLocation/ZLocation.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'ZLocation' 3 | # 4 | # Generated by: sevoroby 5 | # 6 | # Generated on: 12/10/2014 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = 'ZLocation.psm1' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '1.4.3' 16 | 17 | # ID used to uniquely identify this module 18 | GUID = '18e8ca17-7f67-4f1c-85ff-159373bf66f5' 19 | 20 | # Author of this module 21 | Author = 'Sergei Vorobev' 22 | 23 | # Copyright statement for this module 24 | Copyright = '(c) 2015-2018 Sergei Vorobev. All rights reserved.' 25 | 26 | # Description of the functionality provided by this module 27 | Description = 'ZLocation is the new Jump-Location. A `cd` that learns.' 28 | 29 | # Minimum version of the Windows PowerShell engine required by this module 30 | # PowerShellVersion = '' 31 | 32 | # Name of the Windows PowerShell host required by this module 33 | # PowerShellHostName = '' 34 | 35 | # Minimum version of the Windows PowerShell host required by this module 36 | # PowerShellHostVersion = '' 37 | 38 | # Minimum version of Microsoft .NET Framework required by this module 39 | # DotNetFrameworkVersion = '' 40 | 41 | # Minimum version of the common language runtime (CLR) required by this module 42 | # CLRVersion = '' 43 | 44 | # Processor architecture (None, X86, Amd64) required by this module 45 | # ProcessorArchitecture = '' 46 | 47 | # Modules that must be imported into the global environment prior to importing this module 48 | # RequiredModules = @() 49 | 50 | # Assemblies that must be loaded prior to importing this module 51 | RequiredAssemblies = @("LiteDB\LiteDB.dll") 52 | 53 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 54 | # ScriptsToProcess = @() 55 | 56 | # Type files (.ps1xml) to be loaded when importing this module 57 | # TypesToProcess = @() 58 | 59 | # Format files (.ps1xml) to be loaded when importing this module 60 | # FormatsToProcess = @() 61 | 62 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 63 | # This creates additional scopes and Pester cannot mock cmdlets in those scopes. We use Import-Module directly in Zlocation.psm1 instead. 64 | # NestedModules = @("ZLocation.Storage.psm1", "ZLocation.Search.psm1") 65 | 66 | # Functions to export from this module 67 | FunctionsToExport = @( 68 | 'Get-ZLocation', 69 | 'Invoke-ZLocation', 70 | 'Pop-ZLocation', 71 | 'Remove-ZLocation', 72 | 'Set-ZLocation', 73 | 'Update-ZLocation', 74 | 'Clear-NonExistentZLocation' 75 | ) 76 | 77 | # Cmdlets to export from this module 78 | CmdletsToExport = @() 79 | 80 | # Variables to export from this module 81 | VariablesToExport = @() 82 | 83 | # Aliases to export from this module 84 | AliasesToExport = @( 85 | 'z' 86 | ) 87 | 88 | # List of all modules packaged with this module 89 | # ModuleList = @() 90 | 91 | # List of all files packaged with this module 92 | # FileList = @() 93 | 94 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 95 | PrivateData = @{ 96 | 97 | PSData = @{ 98 | 99 | # Tags applied to this module. These help with module discovery in online galleries. 100 | # Tags = @() 101 | 102 | # A URL to the license for this module. 103 | LicenseUri = 'https://github.com/vors/ZLocation/blob/master/LICENSE' 104 | 105 | # A URL to the main website for this project. 106 | ProjectUri = 'https://github.com/vors/ZLocation' 107 | 108 | # A URL to an icon representing this module. 109 | # IconUri = '' 110 | 111 | # ReleaseNotes of this module 112 | ReleaseNotes = 'https://github.com/vors/ZLocation/blob/master/CHANGELOG.md' 113 | 114 | } # End of PSData hashtable 115 | 116 | } # End of PrivateData hashtable 117 | 118 | # HelpInfo URI of this module 119 | # HelpInfoURI = '' 120 | 121 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 122 | # DefaultCommandPrefix = '' 123 | 124 | } 125 | -------------------------------------------------------------------------------- /ZLocation/ZLocation.psm1: -------------------------------------------------------------------------------- 1 | Set-StrictMode -Version Latest 2 | 3 | # Listing nested modules in .psd1 creates additional scopes and Pester cannot mock cmdlets in those scopes. 4 | # Instead we import them here which works. 5 | Import-Module "$PSScriptRoot\ZLocation.Service.psd1" 6 | Import-Module "$PSScriptRoot\ZLocation.Search.psm1" 7 | Import-Module "$PSScriptRoot\ZLocation.Storage.psm1" 8 | 9 | # I currently consider number of commands executed in directory to be a better metric, than total time spent in a directory. 10 | # See [corresponding issue](https://github.com/vors/ZLocation/issues/6) for details. 11 | # If you prefer the old behavior, uncomment this code. 12 | <# 13 | # 14 | # Weight function. 15 | # 16 | function Update-ZLocation([string]$path) 17 | { 18 | $now = [datetime]::Now 19 | if (Test-Path variable:global:__zlocation_current) 20 | { 21 | $prev = $global:__zlocation_current 22 | $weight = $now.Subtract($prev.Time).TotalSeconds 23 | Add-ZWeight ($prev.Location) $weight 24 | } 25 | 26 | $global:__zlocation_current = @{ 27 | Location = $path 28 | Time = [datetime]::Now 29 | } 30 | 31 | # populate folder immediately after the first cd 32 | Add-ZWeight $path 0 33 | } 34 | 35 | # this approach hurts `cd` performance (0.0008 sec vs 0.025 sec). 36 | # Consider replacing it with OnIdle Event. 37 | (Get-Variable pwd).attributes.Add((new-object ValidateScript { Update-ZLocation $_.Path; return $true })) 38 | #> 39 | 40 | function Update-ZLocation 41 | { 42 | param ( 43 | [Parameter(Mandatory=$true)] [string]$Path 44 | ) 45 | Add-ZWeight $path 1.0 46 | } 47 | 48 | function Register-PromptHook 49 | { 50 | param() 51 | 52 | # Insert a call to Update-Zlocation in the prompt function but only once. 53 | if (-not (Test-Path function:\global:ZlocationOrigPrompt)) { 54 | Copy-Item function:\prompt function:\global:ZlocationOrigPrompt 55 | $global:ZLocationPromptScriptBlock = { 56 | Update-ZLocation $pwd 57 | ZLocationOrigPrompt 58 | } 59 | 60 | Set-Content -Path function:\prompt -Value $global:ZLocationPromptScriptBlock -Force 61 | } 62 | } 63 | 64 | # On removal/unload of the module, restore original prompt or LocationChangedAction event handler. 65 | $ExecutionContext.SessionState.Module.OnRemove = { 66 | Copy-Item function:\global:ZlocationOrigPrompt function:\global:prompt 67 | Remove-Item function:\ZlocationOrigPrompt 68 | Remove-Variable ZLocationPromptScriptBlock -Scope Global 69 | } 70 | 71 | # 72 | # End of weight function. 73 | # 74 | 75 | 76 | # 77 | # Tab completion. 78 | # 79 | if(Get-Command -Name Register-ArgumentCompleter -ErrorAction Ignore) { 80 | 'Set-ZLocation', 'Invoke-ZLocation' | % { 81 | Register-ArgumentCompleter -CommandName $_ -ParameterName match -ScriptBlock { 82 | param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) 83 | # Omit first item (command name) and empty strings 84 | $i = $commandAst.CommandElements.Count 85 | [string[]]$query = if($i -gt 1) { 86 | $commandAst.CommandElements[1..($i-1)] | ForEach-Object { $_.toString()} 87 | } 88 | Find-Matches (Get-ZLocation) $query | Get-EscapedPath 89 | } 90 | } 91 | } 92 | 93 | function Get-EscapedPath 94 | { 95 | param( 96 | [Parameter( 97 | Position=0, 98 | Mandatory=$true, 99 | ValueFromPipeline=$true, 100 | ValueFromPipelineByPropertyName=$true) 101 | ] 102 | [string]$path 103 | ) 104 | 105 | process { 106 | if ($path.Contains(' ')) 107 | { 108 | return '"' + $path + '"' 109 | } 110 | return $path 111 | } 112 | } 113 | 114 | # 115 | # End of tab completion. 116 | # 117 | 118 | # Default location stack is local for module. Users cannot use 'Pop-Location' directly, so we need to provide a command inside the module for that. 119 | function Pop-ZLocation 120 | { 121 | Pop-Location 122 | } 123 | 124 | function Set-ZLocation([Parameter(ValueFromRemainingArguments)][string[]]$match) 125 | { 126 | Register-PromptHook 127 | 128 | if (-not $match) { 129 | $match= @() 130 | } 131 | 132 | # Special case to enable Pop-Location. 133 | if (($match.Count -eq 1) -and ($match[0] -eq '-')) { 134 | Pop-ZLocation 135 | return 136 | } 137 | 138 | $matches = Find-Matches (Get-ZLocation) $match 139 | $pushDone = $false 140 | foreach ($m in $matches) { 141 | if (Test-path $m) { 142 | Push-Location $m 143 | $pushDone = $true 144 | break 145 | } else { 146 | Write-Warning "There is no path $m on the file system. Removing obsolete data from database." 147 | Remove-ZLocation $m 148 | } 149 | } 150 | if (-not $pushDone) { 151 | if (($match.Count -eq 1) -and (Test-Path "$match")) { 152 | Write-Debug "No matches for $match, attempting Push-Location" 153 | Push-Location "$match" 154 | } else { 155 | Write-Warning "Cannot find matching location" 156 | } 157 | } 158 | } 159 | 160 | <# 161 | .SYNOPSIS 162 | Jump to popular directories. 163 | 164 | .DESCRIPTION 165 | This is the main entry point in the interactive usage of ZLocation. 166 | It's intended to be used as an alias z. 167 | 168 | Usage: 169 | z - prints available directories 170 | z -l foo - prints available directories scoped to foo query 171 | z foo - jumps into the location that matches foo 172 | z foo - pressing tab cycles through different matches for foo 173 | z - - undos the last z jump. 174 | 175 | .EXAMPLE 176 | PS>z 177 | Weight Path 178 | ------ ---- 179 | 7 C:\Windows 180 | 1 C:\Windows\System 181 | 27 C:\WINDOWS\system32 182 | [...] 183 | 184 | .EXAMPLE 185 | C:\>z foo 186 | C:\foo> 187 | 188 | .EXAMPLE 189 | C:\>z foo 190 | C:\>z C:\foo 191 | C:\>z C:\another_less_popular\foo 192 | C:\>z C:\least_popular\foo 193 | C:\least_popular\foo> 194 | 195 | .EXAMPLE 196 | PS>z -l sys 197 | Weight Path 198 | ------ ---- 199 | 27 C:\WINDOWS\system32 200 | 1 C:\Windows\System 201 | 202 | .EXAMPLE 203 | C:\>z foo 204 | C:\foo>z bar 205 | C:\baz\bar> z - 206 | C:\foo>z - 207 | C:\>z - 208 | C:\>#no-op 209 | 210 | .LINK 211 | https://github.com/vors/ZLocation 212 | #> 213 | function Invoke-ZLocation 214 | { 215 | param( 216 | [Parameter(ValueFromRemainingArguments)][string[]]$match 217 | ) 218 | 219 | $sortProperty = "Path" 220 | $sortDescending = $false 221 | 222 | $locations = $null 223 | if ($null -eq $match) { 224 | $locations = Get-ZLocation 225 | } 226 | elseif (($match.Length -gt 0) -and ($match[0] -eq '-l')) { 227 | $locations = Get-ZLocation ($match | Select-Object -Skip 1) 228 | $sortProperty = "Weight" 229 | $sortDescending = $true 230 | } 231 | 232 | if ($locations) { 233 | $locations | 234 | ForEach-Object {$_.GetEnumerator()} | 235 | ForEach-Object {[PSCustomObject]@{Weight = $_.Value; Path = $_.Name}} | 236 | Sort-Object -Property $sortProperty -Descending:$sortDescending 237 | return 238 | } 239 | 240 | Set-ZLocation -match $match 241 | } 242 | 243 | function Get-FrequentFolders { 244 | If (((Get-Variable IsWindows -ErrorAction Ignore | Select-Object -ExpandProperty Value) -or 245 | $PSVersionTable.PSVersion.Major -lt 6) -and ([environment]::OSVersion.Version.Major -ge 10)) { 246 | if (-not $ExecutionContext.SessionState.LanguageMode -eq 'ConstrainedLanguage') { 247 | $QuickAccess = New-Object -ComObject shell.application 248 | $QuickAccess.Namespace("shell:::{679f85cb-0220-4080-b29b-5540cc05aab6}").Items() | 249 | Where-Object IsFolder | 250 | Select-Object -ExpandProperty Path 251 | } 252 | } 253 | } 254 | 255 | function Clear-NonExistentZLocation { 256 | $paths=(Get-ZLocation).Keys 257 | foreach ($path in $paths) 258 | { 259 | if (!(Test-Path $path)) { 260 | Remove-ZLocation $path 261 | Write-Host $path "removed from ZLocation" 262 | } 263 | } 264 | } 265 | 266 | Get-FrequentFolders | ForEach-Object { 267 | if (Test-Path $_) { 268 | Add-ZWeight -Path $_ -Weight 0 269 | } 270 | } 271 | 272 | Register-PromptHook 273 | 274 | Set-Alias -Name z -Value Invoke-ZLocation 275 | 276 | Export-ModuleMember -Function @('Invoke-ZLocation', 'Set-ZLocation', 'Get-ZLocation', 'Pop-ZLocation', 'Remove-ZLocation', 'Clear-NonexistentZLocation') -Alias z 277 | # export this function to make it accessible from prompt 278 | Export-ModuleMember -Function Update-ZLocation 279 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.4.3.{build} 2 | 3 | image: 4 | - Visual Studio 2015 5 | - Visual Studio 2017 6 | - Visual Studio 2019 7 | - Ubuntu 8 | - macOS 9 | 10 | install: 11 | - ps: Install-Module Pester -MinimumVersion 4.0 -MaximumVersion 4.99 -Scope CurrentUser -Force 12 | 13 | build: false 14 | 15 | test_script: 16 | - ps: | 17 | Import-Module Pester -MinimumVersion 4.0 -MaximumVersion 4.99 18 | $testResultsFile = ".\TestsResults.xml" 19 | $res = Invoke-Pester -OutputFormat NUnitXml -OutputFile $testResultsFile -PassThru 20 | (New-Object 'System.Net.WebClient').UploadFile("https://ci.appveyor.com/api/testresults/nunit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path $testResultsFile)) 21 | if ($res.FailedCount -gt 0) { 22 | throw "$($res.FailedCount) tests failed." 23 | } 24 | 25 | deploy: off 26 | 27 | on_finish: 28 | - ps: | 29 | # Only publish one artifact 30 | if($IsWindows -eq $False) { return } 31 | $stagingDirectory = (Resolve-Path .).Path 32 | $zipFile = Join-Path $stagingDirectory "$(Split-Path $pwd -Leaf).zip" 33 | Add-Type -assemblyname System.IO.Compression.FileSystem 34 | [System.IO.Compression.ZipFile]::CreateFromDirectory((Join-Path $pwd 'ZLocation'), $zipFile) 35 | @( 36 | # You can add other artifacts here 37 | (ls $zipFile) 38 | ) | % { Push-AppveyorArtifact $_.FullName } 39 | --------------------------------------------------------------------------------