├── deploy.cmd ├── .nuget ├── NuGet.exe ├── NuGet.Config └── NuGet.targets ├── debug.psc1 ├── Jump.Location ├── debug.psc1 ├── LocationNotFoundException.cs ├── TabExpansion.ps1 ├── Types.ps1xml ├── JumpLocationSnapIn.cs ├── DirectoryWaitPeriod.cs ├── Jump.Location.Format.ps1xml ├── Load.ps1 ├── Database.cs ├── Properties │ └── AssemblyInfo.cs ├── Install.ps1 ├── Record.cs ├── Jump.Location.psd1 ├── SetJumpLocationCommand.cs ├── GetJumpStatusCommand.cs ├── Jump.Location.csproj ├── FileStoreProvider.cs └── CommandController.cs ├── publish.ps1 ├── Jump.Location.Specs ├── packages.config ├── DirectoryWaitPeriodSpec.cs ├── RecordSpec.cs ├── Properties │ └── AssemblyInfo.cs ├── DatabaseSpec.cs ├── Jump.Location.Specs.csproj ├── FileStoreProviderSpec.cs └── CommandControllerSpec.cs ├── Chocolatey ├── tools │ └── chocolateyInstall.ps1 └── Jump-Location.nuspec ├── .travis.yml ├── .gitattributes ├── copyBuild.ps1 ├── License.md ├── Jump.Location.sln ├── .gitignore └── Readme.md /deploy.cmd: -------------------------------------------------------------------------------- 1 | cp .\Build\* %USERPROFILE%\Documents\WindowsPowerShell\Modules\Jump-location -------------------------------------------------------------------------------- /.nuget/NuGet.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkellogg/Jump-Location/HEAD/.nuget/NuGet.exe -------------------------------------------------------------------------------- /.nuget/NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /debug.psc1: -------------------------------------------------------------------------------- 1 | 2 | 3 | 2.0 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Jump.Location/debug.psc1: -------------------------------------------------------------------------------- 1 | 2 | 3 | 2.0 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /publish.ps1: -------------------------------------------------------------------------------- 1 | $version = "0.6.0" 2 | # zip and push binaries to SourceForge first... 3 | 4 | .\.nuget\nuget pack "Chocolatey\Jump-Location.nuspec" -Version $version -OutputDirectory Chocolatey 5 | # now upload .nupkg to Chocolatey.org... 6 | # ? 7 | # profit! -------------------------------------------------------------------------------- /Jump.Location.Specs/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Chocolatey/tools/chocolateyInstall.ps1: -------------------------------------------------------------------------------- 1 | $name = "Jump-Location" 2 | $url = "http://sourceforge.net/projects/jumplocation/files/Jump-Location-0.6.0.zip/download" 3 | $unzipLocation = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)" 4 | 5 | Install-ChocolateyZipPackage $name $url $unzipLocation 6 | 7 | $installer = Join-Path $unziplocation 'Install.ps1' 8 | & $installer -------------------------------------------------------------------------------- /Jump.Location/LocationNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Jump.Location 4 | { 5 | public class LocationNotFoundException : Exception 6 | { 7 | public LocationNotFoundException(string query) 8 | :base(string.Format("Could not find find a suitable match for search query '{0}' in database", query)) 9 | { 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | solution: Jump.Location.sln 3 | install: 4 | - sudo apt-get install -y powershell 5 | - nuget restore Jump.Location.sln 6 | - nuget install xunit.runners -Version 1.9.2 -OutputDirectory testrunner 7 | script: 8 | - xbuild /p:Configuration=Release Jump.Location.sln 9 | - mono ./testrunner/xunit.runners.1.9.2/tools/xunit.console.clr4.exe ./Jump.Location.Specs/bin/Release/Jump.Location.Specs.dll 10 | -------------------------------------------------------------------------------- /Jump.Location/TabExpansion.ps1: -------------------------------------------------------------------------------- 1 | 2 | if (Test-Path Function:\TabExpansion) { 3 | Rename-Item Function:\TabExpansion PreJumpTabExpansion 4 | } 5 | 6 | function global:TabExpansion($line, $lastWord) { 7 | switch -regex ($line) { 8 | "^(Set-JumpLocation|j|xj) .*" { 9 | [Jump.Location.SetJumpLocationCommand]::GetTabExpansion($line, $lastWord) 10 | } 11 | default { 12 | if (Test-Path Function:\PreJumpTabExpansion) { 13 | PreJumpTabExpansion $line $lastWord 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /Jump.Location/Types.ps1xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Jump.Location.IRecord 5 | 6 | 7 | PSStandardMembers 8 | 9 | 10 | DefaultDisplayPropertySet 11 | 12 | Weight 13 | Path 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Jump.Location/JumpLocationSnapIn.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Management.Automation; 3 | 4 | namespace Jump.Location 5 | { 6 | [RunInstaller(true)] 7 | public class JumpLocationSnapIn : PSSnapIn 8 | { 9 | public override string Name 10 | { 11 | get { return "Jump-Location"; } 12 | } 13 | 14 | public override string Vendor 15 | { 16 | get { return "Tim Kellogg"; } 17 | } 18 | 19 | public override string Description 20 | { 21 | get { return "Like Set-Location but reads your mind"; } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /copyBuild.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | copy files from bin folder to Build folder 4 | #> 5 | 6 | if (-not $(Test-Path Build)) { 7 | $null = mkdir Build 8 | } 9 | 10 | $source = "Jump.Location\bin\Debug" 11 | $destination = "Build" 12 | 13 | Write-Host "Copy bits from $source to $destination" 14 | 15 | cp Readme.md $destination 16 | cp License.md $destination 17 | 18 | cp $source\Jump.Location.dll $destination 19 | cp $source\Jump.Location.psd1 $destination 20 | cp $source\Jump.Location.Format.ps1xml $destination 21 | cp $source\Types.ps1xml $destination 22 | cp $source\Install.ps1 $destination 23 | cp $source\Load.ps1 $destination 24 | cp $source\TabExpansion.ps1 $destination 25 | -------------------------------------------------------------------------------- /Jump.Location/DirectoryWaitPeriod.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Jump.Location 4 | { 5 | class DirectoryWaitPeriod 6 | { 7 | private readonly IRecord record; 8 | private readonly DateTime startTime; 9 | private bool hasDestroyed; 10 | 11 | public DirectoryWaitPeriod(IRecord record, DateTime now) 12 | { 13 | this.record = record; 14 | startTime = now; 15 | } 16 | 17 | public void CloseAndUpdate() 18 | { 19 | if (hasDestroyed) 20 | { 21 | // ignore, don't throw error messages. 22 | return; 23 | } 24 | 25 | record.AddTimeSpent(DateTime.Now - startTime); 26 | hasDestroyed = true; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /Jump.Location.Specs/DirectoryWaitPeriodSpec.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Moq; 3 | using Xunit; 4 | using Should; 5 | 6 | namespace Jump.Location.Specs 7 | { 8 | public class DirectoryWaitPeriodSpec 9 | { 10 | [Fact] 11 | public void It_updates_the_records_weight_upon_CloseAndUpdate() 12 | { 13 | var now = DateTime.Now.AddMinutes(-3); 14 | var recordMock = new Mock(); 15 | var waitPeriod = new DirectoryWaitPeriod(recordMock.Object, now); 16 | 17 | waitPeriod.CloseAndUpdate(); 18 | recordMock.Verify(x => x.AddTimeSpent(It.IsAny())); 19 | } 20 | 21 | [Fact] 22 | public void You_cant_call_CloseAndUpdate_twice() 23 | { 24 | var waitPeriod = new DirectoryWaitPeriod(Mock.Of(), DateTime.Now); 25 | waitPeriod.CloseAndUpdate(); 26 | Assert.Throws(() => waitPeriod.CloseAndUpdate()); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Tim Kellogg 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Jump.Location/Jump.Location.Format.ps1xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Jump.Location.Record 6 | 7 | Jump.Location.IRecord 8 | Jump.Location.Record 9 | 10 | 11 | 12 | 13 | 15 14 | 15 | 16 | 75 17 | 18 | 19 | 20 | 21 | 22 | 23 | Weight 24 | 25 | 26 | Path 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /Chocolatey/Jump-Location.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Jump-Location 5 | 0.6.0 6 | tkellogg 7 | LCHarold 8 | https://github.com/tkellogg/Jump-Location/blob/master/License.md 9 | https://github.com/tkellogg/Jump-Location 10 | false 11 | Jump-Location is a faster way to navigate your filesystem. It works by maintaining a database of the directories you use the most from the command line. 12 | 13 | Directories must be visited first before they can be jumped to. 14 | A faster way to navigate your filesystem. 15 | Jump-Location is a PowerShell implementation of the *nix autojump command. 16 | jump location jump-location powershell ps autojump 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Jump.Location/Load.ps1: -------------------------------------------------------------------------------- 1 | # Install.ps1 2 | # 3 | # 1. Install Jump-Location 4 | # 2. Setup alias j 5 | # 3. Setup jumpstat alias (function) 6 | # 4. Start watching directory changes 7 | # 5. Register tab expansion 8 | 9 | $fullpath = Split-Path -Parent $MyInvocation.MyCommand.Path 10 | $dllpath = $fullpath + "\Jump.Location.dll" 11 | 12 | if (-Not (Get-Command Jump-Location -ErrorAction SilentlyContinue)) { 13 | 14 | Import-Module $dllpath -Global -DisableNameChecking 15 | New-Alias -Name j -Value Set-JumpLocation -Scope Global 16 | 17 | # this alias is for backward compatability 18 | New-Alias -Name Jump-Location -Value Set-JumpLocation -Scope Global 19 | 20 | New-Alias -Name jumpstat -Value Get-JumpStatus -Scope Global 21 | 22 | function global:getj { 23 | Param ( 24 | [Parameter(ValueFromRemainingArguments=$true)] $args 25 | ) 26 | jumpstat -First @args 27 | } 28 | 29 | function global:xj { 30 | Param ( 31 | [Parameter(ValueFromRemainingArguments=$true)] $args 32 | ) 33 | explorer $(jumpstat -First @args) 34 | } 35 | 36 | Set-JumpLocation -Initialize 37 | 38 | . $($fullpath + "\TabExpansion.ps1") 39 | 40 | } else { 41 | Write-Warning "Jump-Location already loaded" 42 | } 43 | -------------------------------------------------------------------------------- /Jump.Location/Database.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace Jump.Location 5 | { 6 | public interface IDatabase 7 | { 8 | IEnumerable Records { get; } 9 | void Add(string fullPath); 10 | void Add(IRecord record); 11 | bool Remove(IRecord record); 12 | IRecord GetByFullName(string fullName); 13 | } 14 | 15 | class Database : IDatabase 16 | { 17 | readonly List records = new List(); 18 | 19 | public IEnumerable Records { get { return records; } } 20 | 21 | public void Add(string fullPath) 22 | { 23 | records.Add(new Record(fullPath)); 24 | } 25 | 26 | public void Add(IRecord record) 27 | { 28 | records.Add(record); 29 | } 30 | 31 | public bool Remove(IRecord record) 32 | { 33 | return records.Remove(record); 34 | } 35 | 36 | public IRecord GetByFullName(string fullName) 37 | { 38 | var record = records.FirstOrDefault(x => x.FullName == fullName); 39 | 40 | if (record == null) 41 | { 42 | record = new Record(fullName); 43 | Add(record); 44 | } 45 | 46 | return record; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Jump.Location.Specs/RecordSpec.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Should; 3 | using Xunit; 4 | using Xunit.Extensions; 5 | 6 | namespace Jump.Location.Specs 7 | { 8 | public class RecordSpec 9 | { 10 | public class DescribeFullName 11 | { 12 | [Fact] 13 | public void It_splits_the_supplied_path_on_the_cons() 14 | { 15 | var record = new Record("provider::path", 0); 16 | record.Provider.ShouldEqual("provider"); 17 | record.Path.ShouldEqual("path"); 18 | } 19 | 20 | [Theory] 21 | [InlineData("C:\\debug\\")] 22 | [InlineData("")] 23 | [InlineData("::")] 24 | [InlineData("::C:\\debug")] 25 | [InlineData("C:\\debug::")] 26 | public void It_throws_ArgumentException_when_path_is_invalid(string path) 27 | { 28 | Assert.Throws(() => new Record(path, 0)); 29 | } 30 | } 31 | 32 | public class DescribePathSegments 33 | { 34 | [Fact] 35 | public void It_splits_Path_into_segments_separated_by_backslash_and_lowercased() 36 | { 37 | var record = new Record(@"Provider::C:\Program Files (x86)\Alteryx\Engine"); 38 | record.PathSegments.ShouldEqual(new[] {"c:", "program files (x86)", "alteryx", "engine"}); 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Jump.Location.Specs/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Jump.Location.Specs")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("Hewlett-Packard")] 12 | [assembly: AssemblyProduct("Jump.Location.Specs")] 13 | [assembly: AssemblyCopyright("Copyright © Hewlett-Packard 2012")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("360fefbe-2766-4418-8097-bb13a2ee7a1f")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /Jump.Location/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Jump.Location")] 9 | [assembly: AssemblyDescription("Powershell Cmdlet to jump around")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("Tim Kellogg")] 12 | [assembly: AssemblyProduct("Jump.Location")] 13 | [assembly: AssemblyCopyright("Copyright © Tim Kellogg 2012")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("f0e2fb34-f28f-4fd9-bbf2-0e58ec55b429")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("0.6.0.0")] 36 | [assembly: AssemblyFileVersion("0.6.0.0")] 37 | [assembly: InternalsVisibleTo("Jump.Location.Specs")] 38 | -------------------------------------------------------------------------------- /Jump.Location.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2012 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jump.Location", "Jump.Location\Jump.Location.csproj", "{813430AC-0181-4ACB-B731-C5F6578CEC64}" 5 | EndProject 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jump.Location.Specs", "Jump.Location.Specs\Jump.Location.Specs.csproj", "{C0A231D4-4E62-4E22-BD81-EB8257B15D21}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{4B6F34CE-160F-4857-82B2-6369A41AA07B}" 9 | ProjectSection(SolutionItems) = preProject 10 | .nuget\NuGet.exe = .nuget\NuGet.exe 11 | .nuget\NuGet.targets = .nuget\NuGet.targets 12 | EndProjectSection 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {813430AC-0181-4ACB-B731-C5F6578CEC64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {813430AC-0181-4ACB-B731-C5F6578CEC64}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {813430AC-0181-4ACB-B731-C5F6578CEC64}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {813430AC-0181-4ACB-B731-C5F6578CEC64}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {C0A231D4-4E62-4E22-BD81-EB8257B15D21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {C0A231D4-4E62-4E22-BD81-EB8257B15D21}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {C0A231D4-4E62-4E22-BD81-EB8257B15D21}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {C0A231D4-4E62-4E22-BD81-EB8257B15D21}.Release|Any CPU.Build.0 = Release|Any CPU 28 | EndGlobalSection 29 | GlobalSection(SolutionProperties) = preSolution 30 | HideSolutionNode = FALSE 31 | EndGlobalSection 32 | EndGlobal 33 | -------------------------------------------------------------------------------- /Jump.Location/Install.ps1: -------------------------------------------------------------------------------- 1 | # Adapted from posh-git's install.ps1 2 | # Copyright (c) 2010-2011 Keith Dahlby and contributors 3 | 4 | param( 5 | [switch]$WhatIf = $false, 6 | [string] $WhichProfile = $PROFILE.CurrentUserCurrentHost 7 | ) 8 | 9 | if($PSVersionTable.PSVersion.Major -lt 3) { 10 | Write-Warning "Jump-Location requires PowerShell 3.0 or better; you have version $($Host.Version)." 11 | return 12 | } 13 | 14 | if(!(Test-Path $WhichProfile)) { 15 | Write-Host "Creating PowerShell profile...`n$WhichProfile" 16 | New-Item $WhichProfile -Force -Type File -ErrorAction Stop -WhatIf:$WhatIf > $null 17 | } 18 | 19 | $installDir = Split-Path $MyInvocation.MyCommand.Path -Parent 20 | 21 | # Adapted from http://www.west-wind.com/Weblog/posts/197245.aspx 22 | function Get-FileEncoding($Path) { 23 | $bytes = [byte[]](Get-Content $Path -Encoding byte -ReadCount 4 -TotalCount 4) 24 | 25 | if(!$bytes) { return 'utf8' } 26 | 27 | switch -regex ('{0:x2}{1:x2}{2:x2}{3:x2}' -f $bytes[0],$bytes[1],$bytes[2],$bytes[3]) { 28 | '^efbbbf' { return 'utf8' } 29 | '^2b2f76' { return 'utf7' } 30 | '^fffe' { return 'unicode' } 31 | '^feff' { return 'bigendianunicode' } 32 | '^0000feff' { return 'utf32' } 33 | default { return 'ascii' } 34 | } 35 | } 36 | 37 | $profileLine = "Import-Module '$installDir\Jump.Location.psd1'" 38 | if(Select-String -Path $WhichProfile -Pattern $profileLine -Quiet -SimpleMatch) { 39 | Write-Host "It seems Jump-Location is already installed..." 40 | return 41 | } 42 | 43 | Write-Host "Adding Jump-Location to profile..." 44 | @" 45 | 46 | # Load Jump-Location profile 47 | $profileLine 48 | 49 | "@ | Out-File $WhichProfile -Append -WhatIf:$WhatIf -Encoding (Get-FileEncoding $WhichProfile) 50 | 51 | Write-Host 'Jump-Location sucessfully installed!' 52 | Write-Host 'Please reload your profile for the changes to take effect:' 53 | Write-Host " . $WhichProfile" -------------------------------------------------------------------------------- /Jump.Location/Record.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace Jump.Location 5 | { 6 | public interface IRecord 7 | { 8 | string Provider { get; } 9 | string Path { get; } 10 | string FullName { get; set; } 11 | decimal Weight { get; } 12 | string[] PathSegments { get; } 13 | void AddTimeSpent(TimeSpan timeSpan); 14 | } 15 | 16 | public class Record : IRecord 17 | { 18 | private string[] pathSegments; 19 | 20 | public Record(string fullName, decimal weight) 21 | { 22 | FullName = fullName; 23 | Weight = weight; 24 | } 25 | 26 | public Record(string fullName) 27 | :this(fullName, 0) 28 | { 29 | } 30 | 31 | public string Provider { get; private set; } 32 | public string Path { get; private set; } 33 | 34 | public string FullName 35 | { 36 | get { return string.Format("{0}::{1}", Provider, Path); } 37 | set 38 | { 39 | var parts = value.Split(new[] { "::" }, StringSplitOptions.RemoveEmptyEntries); 40 | 41 | if (parts == null || parts.Length != 2) 42 | throw new ArgumentException("Expected FullName in format of 'Provider::Path' but got " + value); 43 | 44 | Provider = parts[0]; 45 | Path = parts[1]; 46 | } 47 | } 48 | 49 | public decimal Weight { get; set; } 50 | 51 | public string[] PathSegments 52 | { 53 | get { return pathSegments ?? (pathSegments = GetPathSegments()); } 54 | } 55 | 56 | private string[] GetPathSegments() 57 | { 58 | return Path.Split('\\').Select(x => x.ToLower()).ToArray(); 59 | } 60 | 61 | public void AddTimeSpent(TimeSpan timeSpan) 62 | { 63 | Weight += (decimal) timeSpan.TotalSeconds; 64 | } 65 | 66 | public override string ToString() 67 | { 68 | return Path; 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /Jump.Location.Specs/DatabaseSpec.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Moq; 3 | using Should; 4 | using Xunit; 5 | 6 | namespace Jump.Location.Specs 7 | { 8 | public class DatabaseSpec 9 | { 10 | public class DescribeAdding 11 | { 12 | [Fact] 13 | public void It_adds_a_new_record_by_path() 14 | { 15 | var db = new Database(); 16 | db.Add("full::path"); 17 | db.Records.Count().ShouldEqual(1); 18 | } 19 | 20 | [Fact] 21 | public void It_adds_a_new_record_by_object() 22 | { 23 | var db = new Database(); 24 | db.Add(Mock.Of()); 25 | db.Records.Count().ShouldEqual(1); 26 | } 27 | } 28 | 29 | public class DescribeFindingByFullName 30 | { 31 | [Fact] 32 | public void It_returns_the_matching_record() 33 | { 34 | var db = new Database(); 35 | db.Add(Mock.Of(x => x.FullName == "foo")); 36 | db.Add(Mock.Of(x => x.FullName == "bar")); 37 | 38 | db.GetByFullName("bar").ShouldNotBeNull(); 39 | } 40 | 41 | [Fact] 42 | public void It_creates_a_record_when_missing() 43 | { 44 | var db = new Database(); 45 | db.Add(Mock.Of(x => x.FullName == "foo")); 46 | db.Add(Mock.Of(x => x.FullName == "bar")); 47 | 48 | var baz = db.GetByFullName("foo::bar"); 49 | baz.ShouldNotBeNull(); 50 | baz.ShouldBeType(); 51 | } 52 | 53 | [Fact] 54 | public void It_adds_the_missing_record_to_its_list() 55 | { 56 | var db = new Database(); 57 | db.Add(Mock.Of(x => x.FullName == "foo")); 58 | db.Add(Mock.Of(x => x.FullName == "bar")); 59 | 60 | db.GetByFullName("foo::bar"); 61 | db.Records.Count().ShouldEqual(3); 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Jump.Location/Jump.Location.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'Jump.Location' 3 | # 4 | # Generated by: Tim Kellogg 5 | # 6 | # Generated on: 8/12/2012 7 | # 8 | 9 | @{ 10 | 11 | # Version number of this module. 12 | ModuleVersion = '0.6.0' 13 | 14 | # ID used to uniquely identify this module 15 | GUID = '80410ccb-8093-42a4-b38e-94deef91346e' 16 | 17 | # Author of this module 18 | Author = 'Tim Kellogg' 19 | 20 | # Company or vendor of this module 21 | CompanyName = 'Unknown' 22 | 23 | # Copyright statement for this module 24 | Copyright = '2012 Tim Kellogg' 25 | 26 | # Description of the functionality provided by this module 27 | Description = 'A cd command that learns' 28 | 29 | # Script module or binary module file associated with this manifest 30 | RootModule = 'Load.ps1' 31 | 32 | # Minimum version of the Windows PowerShell engine required by this module 33 | PowerShellVersion = '' 34 | 35 | # Name of the Windows PowerShell host required by this module 36 | PowerShellHostName = '' 37 | 38 | # Minimum version of the Windows PowerShell host required by this module 39 | PowerShellHostVersion = '' 40 | 41 | # Minimum version of the .NET Framework required by this module 42 | DotNetFrameworkVersion = '' 43 | 44 | # Minimum version of the common language runtime (CLR) required by this module 45 | CLRVersion = '' 46 | 47 | # Processor architecture (None, X86, Amd64, IA64) required by this module 48 | ProcessorArchitecture = '' 49 | 50 | # Modules that must be imported into the global environment prior to importing this module 51 | RequiredModules = @() 52 | 53 | # Assemblies that must be loaded prior to importing this module 54 | RequiredAssemblies = @() 55 | 56 | # Script files (.ps1) that are run in the caller's environment prior to importing this module 57 | ScriptsToProcess = @() 58 | 59 | # Type files (.ps1xml) to be loaded when importing this module 60 | TypesToProcess = 'Types.ps1xml' 61 | 62 | # Format files (.ps1xml) to be loaded when importing this module 63 | FormatsToProcess = 'Jump.Location.Format.ps1xml' 64 | 65 | # Modules to import as nested modules of the module specified in ModuleToProcess 66 | NestedModules = @() 67 | 68 | # Functions to export from this module 69 | FunctionsToExport = '*' 70 | 71 | # Cmdlets to export from this module 72 | CmdletsToExport = '*' 73 | 74 | # Variables to export from this module 75 | VariablesToExport = '*' 76 | 77 | # Aliases to export from this module 78 | AliasesToExport = '*' 79 | 80 | # List of all modules packaged with this module 81 | ModuleList = @() 82 | 83 | # List of all files packaged with this module 84 | FileList = @() 85 | 86 | # Private data to pass to the module specified in ModuleToProcess 87 | PrivateData = '' 88 | 89 | } 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | [Dd]ebug/ 46 | [Rr]elease/ 47 | *_i.c 48 | *_p.c 49 | *.ilk 50 | *.meta 51 | *.obj 52 | *.pch 53 | *.pdb 54 | *.pgc 55 | *.pgd 56 | *.rsp 57 | *.sbr 58 | *.tlb 59 | *.tli 60 | *.tlh 61 | *.tmp 62 | *.vspscc 63 | .builds 64 | *.dotCover 65 | 66 | ## TODO: If you have NuGet Package Restore enabled, uncomment this 67 | packages/ 68 | 69 | # Visual C++ cache files 70 | ipch/ 71 | *.aps 72 | *.ncb 73 | *.opensdf 74 | *.sdf 75 | 76 | # Visual Studio profiler 77 | *.psess 78 | *.vsp 79 | 80 | # ReSharper is a .NET coding add-in 81 | _ReSharper* 82 | 83 | # NCrunch 84 | *.ncrunch* 85 | 86 | # Installshield output folder 87 | [Ee]xpress 88 | 89 | # DocProject is a documentation generator add-in 90 | DocProject/buildhelp/ 91 | DocProject/Help/*.HxT 92 | DocProject/Help/*.HxC 93 | DocProject/Help/*.hhc 94 | DocProject/Help/*.hhk 95 | DocProject/Help/*.hhp 96 | DocProject/Help/Html2 97 | DocProject/Help/html 98 | 99 | # Click-Once directory 100 | publish 101 | 102 | # Others 103 | [Bb]in 104 | [Oo]bj 105 | sql 106 | TestResults 107 | *.Cache 108 | ClientBin 109 | stylecop.* 110 | ~$* 111 | *.dbmdl 112 | Generated_Code #added for RIA/Silverlight projects 113 | 114 | # Backup & report files from converting an old project file to a newer 115 | # Visual Studio version. Backup files are not needed, because we have git ;-) 116 | _UpgradeReport_Files/ 117 | Backup*/ 118 | UpgradeLog*.XML 119 | 120 | 121 | 122 | ############ 123 | ## Windows 124 | ############ 125 | 126 | # Windows image file caches 127 | Thumbs.db 128 | 129 | # Folder config file 130 | Desktop.ini 131 | 132 | 133 | ############# 134 | ## Python 135 | ############# 136 | 137 | *.py[co] 138 | 139 | # Packages 140 | *.egg 141 | *.egg-info 142 | dist 143 | build 144 | eggs 145 | parts 146 | bin 147 | var 148 | sdist 149 | develop-eggs 150 | .installed.cfg 151 | 152 | # Installer logs 153 | pip-log.txt 154 | 155 | # Unit test / coverage reports 156 | .coverage 157 | .tox 158 | 159 | #Translations 160 | *.mo 161 | 162 | #Mr Developer 163 | .mr.developer.cfg 164 | 165 | # Mac crap 166 | .DS_Store 167 | -------------------------------------------------------------------------------- /Jump.Location.Specs/Jump.Location.Specs.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {C0A231D4-4E62-4E22-BD81-EB8257B15D21} 8 | Library 9 | Properties 10 | Jump.Location.Specs 11 | Jump.Location.Specs 12 | v4.0 13 | 512 14 | {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 15 | ..\ 16 | true 17 | 18 | 19 | 20 | true 21 | full 22 | false 23 | bin\Debug\ 24 | DEBUG;TRACE 25 | prompt 26 | 4 27 | 28 | 29 | pdbonly 30 | true 31 | bin\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | 36 | 37 | 38 | ..\packages\Moq.4.0.10827\lib\NET40\Moq.dll 39 | 40 | 41 | ..\packages\Should.1.1.12.0\lib\Should.dll 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | ..\packages\xunit.1.9.1\lib\net20\xunit.dll 52 | 53 | 54 | ..\packages\xunit.extensions.1.9.1\lib\net20\xunit.extensions.dll 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | {813430AC-0181-4ACB-B731-C5F6578CEC64} 71 | Jump.Location 72 | 73 | 74 | 75 | 76 | 83 | -------------------------------------------------------------------------------- /Jump.Location/SetJumpLocationCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Globalization; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Management.Automation; 8 | 9 | namespace Jump.Location 10 | { 11 | [Cmdlet(VerbsCommon.Set, "JumpLocation", DefaultParameterSetName = "Query")] 12 | public class SetJumpLocationCommand : PSCmdlet 13 | { 14 | private static readonly CommandController Controller = CommandController.DefaultInstance; 15 | 16 | public static IEnumerable GetTabExpansion(string line, string lastWord) 17 | { 18 | // line is something like "j term1 term2 temr3". 19 | // Skip cmdlet name and call match for the rest. 20 | string[] searchTerms = line.Split().Skip(1).ToArray(); 21 | return Controller.GetMatchesForSearchTerm(searchTerms).Select(GetResultPath); 22 | } 23 | 24 | private static string GetResultPath(IRecord record) 25 | { 26 | var candidate = record.Path; 27 | if (candidate.Contains(" ")) 28 | return string.Format("\"{0}\"", candidate); 29 | return candidate; 30 | } 31 | 32 | [Parameter(ParameterSetName = "Query", ValueFromRemainingArguments = true)] 33 | public string[] Query { get; set; } 34 | 35 | [Parameter(ParameterSetName = "Initialize", 36 | HelpMessage = "Initialize Jump-Location by starting to listen to directory changes.")] 37 | public SwitchParameter Initialize { get; set; } 38 | 39 | public static void UpdateTime(string location) 40 | { 41 | Controller.UpdateLocation(location); 42 | } 43 | 44 | protected override void ProcessRecord() 45 | { 46 | // This lets us do just `Jump-Location -Initialize` to initialize everything in the profile script 47 | if (Initialize) 48 | { 49 | InvokeCommand.InvokeScript(@" 50 | Register-EngineEvent -SourceIdentifier PowerShell.OnIdle -Action { 51 | [Jump.Location.SetJumpLocationCommand]::UpdateTime($($(Get-Item -Path $(Get-Location))).PSPath) 52 | } 53 | "); 54 | return; 55 | } 56 | 57 | if (Query == null) { Query = new string[] {}; } 58 | 59 | // "j -" is an alias for popd. 60 | if (Query.Length == 1 && Query[0] == "-") 61 | { 62 | InvokeCommand.InvokeScript("Pop-Location"); 63 | return; 64 | } 65 | 66 | // If last term is absolute path it's probably because of autocomplition 67 | // we can safely process it here. 68 | if (Query.Any() && Path.IsPathRooted(Query.Last())) 69 | { 70 | if (!Directory.Exists(Query.Last())) 71 | { 72 | throw new LocationNotFoundException(Query.Last()); 73 | } 74 | ChangeDirectory(Query.Last()); 75 | return; 76 | } 77 | 78 | IEnumerable orderedMatches = Controller.GetMatchesForSearchTerm(Query); 79 | if (orderedMatches == null) throw new LocationNotFoundException(String.Join(" ", Query)); 80 | bool destinationFound = false; 81 | foreach (IRecord match in orderedMatches) 82 | { 83 | if (match.Provider == @"Microsoft.PowerShell.Core\FileSystem" && !Directory.Exists(match.Path)) 84 | { 85 | WriteWarning(String.Format("Skipping {0}: directory not found. You can remove obsolete directories from database with command 'jumpstat -cleanup'.", match.Path)); 86 | continue; 87 | } 88 | ChangeDirectory(match.Path); 89 | destinationFound = true; 90 | break; 91 | } 92 | 93 | if (!destinationFound) 94 | { 95 | // ask PowerShell to try to resolve the path (handling for things like "~") 96 | var resolvedPath = GetUnresolvedProviderPathFromPSPath(Query.Last()); 97 | 98 | if (!Directory.Exists(resolvedPath)) 99 | { 100 | throw new LocationNotFoundException(String.Join(" ", Query)); 101 | } 102 | ChangeDirectory(Query.Last()); 103 | } 104 | } 105 | 106 | private void ChangeDirectory(string fullPath) 107 | { 108 | InvokeCommand.InvokeScript(string.Format("Push-Location '{0}'", fullPath.Trim())); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Jump.Location.Specs/FileStoreProviderSpec.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading; 6 | using Should; 7 | using Xunit; 8 | using Xunit.Extensions; 9 | 10 | namespace Jump.Location.Specs 11 | { 12 | public class FileStoreProviderSpec 13 | { 14 | public class DescribeRevive : IDisposable 15 | { 16 | private readonly string path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); 17 | 18 | public void Dispose() 19 | { 20 | try 21 | { 22 | File.Delete(path); 23 | } 24 | catch 25 | { 26 | } 27 | } 28 | 29 | [Fact] 30 | public void It_can_Revive_a_single_record_of_correctly_formatted_text() 31 | { 32 | var lines = new[] {"8.5\tFS::C:\\blah"}; 33 | File.WriteAllLines(path, lines); 34 | var provider = new FileStoreProvider(path); 35 | 36 | var db = provider.Revive(); 37 | db.Records.Count().ShouldEqual(1); 38 | db.Records.First().Weight.ShouldEqual(8.5M); 39 | } 40 | 41 | [Fact] 42 | public void It_can_Revive_multiple_records_of_correctly_formatted_text() 43 | { 44 | var lines = new[] 45 | { 46 | "8.5\tFS::C:\\blah", 47 | "8.5\tFS::C:\\blah", 48 | "8.5\tFS::C:\\blah", 49 | }; 50 | File.WriteAllLines(path, lines); 51 | var provider = new FileStoreProvider(path); 52 | 53 | var db = provider.Revive(); 54 | db.Records.Count().ShouldEqual(3); 55 | } 56 | 57 | [Fact] 58 | public void It_can_Revive_a_record_with_invalid_weight() 59 | { 60 | var lines = new[] {"INVALID_WEIGHT\tFS::C:\\blah"}; 61 | File.WriteAllLines(path, lines); 62 | var provider = new FileStoreProvider(path); 63 | 64 | var db = provider.Revive(); 65 | db.Records.Count().ShouldEqual(1); 66 | db.Records.First().Weight.ShouldEqual(0M); 67 | db.Records.First().Path.ShouldEqual("C:\\blah"); 68 | } 69 | 70 | [Fact] 71 | public void It_skips_blank_and_empty_lines() 72 | { 73 | var lines = new[] 74 | { 75 | "", 76 | "8.5\tFS::C:\\blah", 77 | " ", 78 | }; 79 | File.WriteAllLines(path, lines); 80 | var provider = new FileStoreProvider(path); 81 | 82 | var db = provider.Revive(); 83 | db.Records.Count().ShouldEqual(1); 84 | } 85 | 86 | [Fact] 87 | public void It_throws_InvalidOperationException_when_row_doesnt_have_2_columns() 88 | { 89 | var lines = new[] {"8.5 FS::C:\\blah"}; 90 | File.WriteAllLines(path, lines); 91 | var provider = new FileStoreProvider(path); 92 | 93 | Assert.Throws(() => provider.Revive()); 94 | } 95 | } 96 | 97 | public class DescribeSave : IDisposable 98 | { 99 | private readonly string path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); 100 | 101 | public void Dispose() 102 | { 103 | try 104 | { 105 | File.Delete(path); 106 | } 107 | catch 108 | { 109 | } 110 | } 111 | 112 | [Fact] 113 | public void It_can_save_a_single_record() 114 | { 115 | var db = new Database(); 116 | db.Add(new Record("FS::C:\\data", 42M)); 117 | var provider = new FileStoreProvider(path); 118 | provider.Save(db); 119 | 120 | var contents = File.ReadAllText(path); 121 | contents.ShouldEqual("42\tFS::C:\\data\r\n"); 122 | } 123 | 124 | [Fact] 125 | public void It_can_save_a_single_record_with_weight_as_decimal_value() 126 | { 127 | var db = new Database(); 128 | db.Add(new Record("FS::C:\\data", 42.5M)); 129 | var provider = new FileStoreProvider(path); 130 | provider.Save(db); 131 | 132 | var contents = File.ReadAllText(path); 133 | contents.ShouldEqual("42.5\tFS::C:\\data\r\n"); 134 | } 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /Jump.Location/GetJumpStatusCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Management.Automation; 4 | 5 | namespace Jump.Location 6 | { 7 | using System; 8 | using System.Configuration; 9 | using System.IO; 10 | 11 | [Cmdlet(VerbsCommon.Get, "JumpStatus", DefaultParameterSetName = "Query")] 12 | public class GetJumpStatusCommand : PSCmdlet 13 | { 14 | private static readonly CommandController Controller = CommandController.DefaultInstance; 15 | 16 | [Parameter(ParameterSetName = "Query", ValueFromRemainingArguments = true)] 17 | public string[] Query { get; set; } 18 | 19 | [Parameter(ParameterSetName = "Query", HelpMessage = "Only retrieve the first record matching the query. Same as `getj`")] 20 | public SwitchParameter First { get; set; } 21 | 22 | [Parameter(ParameterSetName = "Save", HelpMessage = "Saves any pending changes from setting weights on records explicitly.")] 23 | public SwitchParameter Save { get; set; } 24 | 25 | [Parameter(ParameterSetName = "Query", HelpMessage = "Includes all results, even if the weight is negative.")] 26 | public SwitchParameter All { get; set; } 27 | 28 | [Parameter(ParameterSetName = "Cleanup", HelpMessage = "Remove obsolete(not existing on the file system) records from DB.")] 29 | public SwitchParameter Cleanup { get; set; } 30 | 31 | [Parameter(ParameterSetName = "Scan", HelpMessage = "Scan and discover subdirectories.")] 32 | [AllowEmptyString] 33 | public string Scan { get; set; } 34 | 35 | protected override void ProcessRecord() 36 | { 37 | 38 | if (this.ParameterSetName == "Cleanup") 39 | { 40 | DoCleanup(); 41 | return; 42 | } 43 | 44 | if (this.ParameterSetName == "Scan") 45 | { 46 | DoScan(); 47 | return; 48 | } 49 | 50 | if (All || Query == null || Query.Length == 0) 51 | { 52 | ProcessSearch(Controller.GetOrderedRecords(All)); 53 | } 54 | else 55 | { 56 | ProcessSearch(Controller.GetMatchesForSearchTerm(Query)); 57 | } 58 | } 59 | 60 | private const string FileSystemProvider = @"Microsoft.PowerShell.Core\FileSystem"; 61 | private void DoCleanup() 62 | { 63 | int recordsRemoved = 0; 64 | foreach (IRecord record in Controller.GetOrderedRecords(true)) 65 | { 66 | if (record.Provider == FileSystemProvider && !Directory.Exists(record.Path)) 67 | { 68 | Controller.RemoveRecord(record); 69 | recordsRemoved++; 70 | } 71 | } 72 | if (recordsRemoved > 0) 73 | { 74 | Controller.Save(); 75 | WriteVerbose("Number of records cleaned: " + recordsRemoved + "."); 76 | } 77 | } 78 | 79 | private void DoScan() 80 | { 81 | if (string.IsNullOrEmpty(this.Scan)) 82 | { 83 | // set default to current directory 84 | this.Scan = "."; 85 | } 86 | this.Scan = GetUnresolvedProviderPathFromPSPath(this.Scan); 87 | WriteVerbose("Discovering new folders."); 88 | 89 | int count = 0; 90 | foreach (string fullPath in GetChildFoldersRec(this.Scan)) 91 | { 92 | WriteVerbose(string.Format("[Touching] {0}", fullPath)); 93 | Controller.TouchRecord(string.Format("{0}::{1}", FileSystemProvider, fullPath)); 94 | count++; 95 | } 96 | 97 | Controller.Save(); 98 | WriteVerbose(string.Format("Number of directories touched: {0}.", count)); 99 | } 100 | 101 | private IEnumerable GetChildFoldersRec(string path) 102 | { 103 | yield return path; 104 | foreach (string dir in Directory.GetDirectories(path)) 105 | { 106 | foreach (string childDir in GetChildFoldersRec(dir)) 107 | { 108 | yield return childDir; 109 | } 110 | } 111 | } 112 | 113 | private void ProcessSearch(IEnumerable records) 114 | { 115 | if (Save) 116 | { 117 | Controller.Save(); 118 | return; 119 | } 120 | 121 | if (First) 122 | { 123 | var record = records.FirstOrDefault(); 124 | if (record != null) 125 | WriteObject(record); 126 | return; 127 | } 128 | 129 | foreach (var record in records) 130 | { 131 | WriteObject(record); 132 | } 133 | WriteVerbose("Number of records: " + records.Count() + "."); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Jump.Location/Jump.Location.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {813430AC-0181-4ACB-B731-C5F6578CEC64} 8 | Library 9 | Properties 10 | Jump.Location 11 | Jump.Location 12 | v3.5 13 | 512 14 | 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | false 26 | 27 | 28 | AnyCPU 29 | pdbonly 30 | true 31 | bin\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | false 36 | 37 | 38 | 39 | 40 | 41 | OnBuildSuccess 42 | 43 | 44 | 45 | False 46 | ..\..\..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\WindowsPowerShell\v1.0\Microsoft.PowerShell.Commands.Management.dll 47 | 48 | 49 | 50 | 51 | 52 | 53 | False 54 | ..\..\..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\WindowsPowerShell\v1.0\System.Management.Automation.dll 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | Component 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | PreserveNewest 78 | 79 | 80 | PreserveNewest 81 | 82 | 83 | PreserveNewest 84 | 85 | 86 | PreserveNewest 87 | 88 | 89 | PreserveNewest 90 | 91 | 92 | PreserveNewest 93 | 94 | 95 | 96 | 97 | PreserveNewest 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 116 | -------------------------------------------------------------------------------- /Jump.Location/FileStoreProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Runtime.InteropServices; 7 | using System.Security.AccessControl; 8 | using System.Security.Principal; 9 | using System.Threading; 10 | 11 | namespace Jump.Location 12 | { 13 | public interface IFileStoreProvider 14 | { 15 | void Save(IDatabase database); 16 | IDatabase Revive(); 17 | DateTime LastChangedDate { get; } 18 | } 19 | 20 | public class FileStoreUpdatedEventArgs : EventArgs 21 | { 22 | public FileStoreUpdatedEventArgs(IDatabase database) 23 | { 24 | UpdatedDatabase = database; 25 | } 26 | 27 | public IDatabase UpdatedDatabase { get; private set; } 28 | } 29 | 30 | public delegate void FileStoreUpdated(object sender, FileStoreUpdatedEventArgs args); 31 | 32 | class FileStoreProvider : IFileStoreProvider 33 | { 34 | private readonly string _path; 35 | private readonly string _pathTemp; 36 | private readonly string _mutexId; 37 | private readonly MutexSecurity _securitySettings; 38 | 39 | private const string TempPrefix = ".tmp"; 40 | 41 | public FileStoreProvider(string path) 42 | { 43 | this._path = path; 44 | this._pathTemp = path + TempPrefix; 45 | 46 | // global mutex implementation from http://stackoverflow.com/questions/229565/what-is-a-good-pattern-for-using-a-global-mutex-in-c 47 | 48 | // get application GUID as defined in AssemblyInfo.cs 49 | string appGuid = ((GuidAttribute)Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(GuidAttribute), false).GetValue(0)).Value.ToString(CultureInfo.InvariantCulture); 50 | 51 | // unique id for global mutex - Global prefix means it is global to the machine 52 | _mutexId = string.Format("Global\\{{{0}}}", appGuid); 53 | 54 | // setting up security for multi-user usage 55 | // work also on localized systems (don't use just "Everyone") 56 | var allowEveryoneRule = new MutexAccessRule(new SecurityIdentifier(WellKnownSidType.WorldSid, null), MutexRights.FullControl, AccessControlType.Allow); 57 | _securitySettings = new MutexSecurity(); 58 | _securitySettings.AddAccessRule(allowEveryoneRule); 59 | } 60 | 61 | public event FileStoreUpdated FileStoreUpdated; 62 | 63 | private void OnFileStoreUpdated(object sender, FileStoreUpdatedEventArgs args) 64 | { 65 | if (FileStoreUpdated != null) 66 | FileStoreUpdated(sender, args); 67 | } 68 | 69 | public void Save(IDatabase database) 70 | { 71 | var lines = database.Records.Select(record => 72 | string.Format("{1}\t{0}", record.FullName, record.Weight.ToString(CultureInfo.InvariantCulture))); 73 | 74 | bool createdNew; 75 | using (var mutex = new Mutex(false, _mutexId, out createdNew, _securitySettings)) 76 | { 77 | var hasHandle = false; 78 | try 79 | { 80 | try 81 | { 82 | hasHandle = mutex.WaitOne(1000, false); 83 | if (hasHandle == false) 84 | { 85 | // ignore 86 | return; 87 | } 88 | } 89 | catch (AbandonedMutexException) 90 | { 91 | // Log the fact the mutex was abandoned in another process, it will still get aquired 92 | hasHandle = true; 93 | } 94 | 95 | // We can lose all history, if powershell will be closed during operation. 96 | File.WriteAllLines(_pathTemp, lines.ToArray()); 97 | // NTFS guarantees atomic move operation http://stackoverflow.com/questions/774098/atomicity-of-file-move 98 | // So File.Move gurantees atomic, but doesn't support overwrite 99 | File.Copy(_pathTemp, _path, true); 100 | } 101 | finally 102 | { 103 | // edited by acidzombie24, added if statemnet 104 | if(hasHandle) 105 | mutex.ReleaseMutex(); 106 | } 107 | } 108 | } 109 | 110 | public IDatabase Revive() 111 | { 112 | var db = new Database(); 113 | var allLines = File.ReadAllLines(_path); 114 | var nonBlankLines = allLines.Where(line => !string.IsNullOrEmpty(line) && line.Trim() != string.Empty); 115 | foreach (var columns in nonBlankLines.Select(line => line.Split('\t'))) 116 | { 117 | if (columns == null || columns.Length != 2) 118 | throw new InvalidOperationException("Row of file didn't have 2 columns separated by a tab"); 119 | 120 | var weight = 0M; 121 | decimal.TryParse(columns[0], NumberStyles.Any, CultureInfo.InvariantCulture, out weight); 122 | var record = new Record(columns[1], weight); 123 | db.Add(record); 124 | } 125 | return db; 126 | } 127 | 128 | public DateTime LastChangedDate { get { return File.GetLastWriteTime(_path); } } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | This project is not actively maintained. Please consider using its successor [`ZLocation`](https://github.com/vors/ZLocation). 2 | ===================== 3 | 4 | 5 | --------------------- 6 | 7 | 8 | Jump-Location: A cd that learns 9 | ===================== 10 | 11 | If you spend any time in a console you know that `cd` is by far the most 12 | common command that you issue. I'll open up a console to it's default location 13 | in `C:\Users\tkellogg` or `C:\Windows\system32` and then issue a `cd C:\work\MyProject`. 14 | `Set-JumpLocation` is a cmdlet lets you issue a `j my` to jump 15 | directly to `C:\work\MyProject`. 16 | 17 | It learns your behavior by keeping track of how long you spend in certain 18 | directories and favoring them over the ones you don't care about. You don't 19 | have to use `Jump-Location` as a replacement for `cd`. Use `cd` to go local, and 20 | use `Set-JumpLocation` to jump further away. 21 | 22 | `Jump-Location` is a powershell implementation of [autojump][1]. 23 | 24 | 25 | How it knows where you want to go 26 | --------------------------------- 27 | 28 | It keeps track of [how long you stay in a directory][2] and builds a database. 29 | When you use the `Set-JumpLocation` or `j` command, it looks through the database 30 | to find the most likely directory and jumps there. You should only need to 31 | give it a 2-3 character hint. For instance, on mine I can do: 32 | 33 | * `j de` -> `C:\Users\tkellogg\code\Jump-Location\bin\Debug` 34 | * `j doc` -> `C:\Users\tkellogg\Documents` 35 | 36 | What if you have several projects and you want to get to the Debug directory 37 | of one that you don't use very often? If you're `jumpstat` looks like this: 38 | 39 | 255 C:\Users\tkellogg\code\Jump-Location\bin\Debug 40 | 50 C:\Users\tkellogg\code\MongoDB.FSharp\bin\Debug 41 | 42 | Using `j de` will jump to `Jump-Location\bin\Debug`. But use something like 43 | `j mo d` if you really want to go to `MongoDB.FSharp\bin\Debug`. You can 44 | issue a `j mo d`. `mo` matches `MongoDB.FSharp` and `d` matches `Debug`. 45 | 46 | `j` internally calls `Push-Location`, so you can navigate back in your visited locations stack with `popd` or special Jump-Location query `j -`. 47 | 48 | Quick Primer on `jumpstat` 49 | -------------------------- 50 | 51 | You can use `jumpstat` to see what's in the database. In tradition with `autojump`, 52 | the database is saved to `~\jump-location.txt`. You can open up that file and 53 | make changes. The file will auto-load into any open powershell sessions. 54 | 55 | Since we're in Powershell (and not legacy Bash) `jumpstat` returns _objects_. 56 | This means that you don't ever have to know anything about `~\jump-location.txt`. 57 | You can manipulate the objects it returns. For instance, this is valid: 58 | 59 | ``` 60 | PS> $stats = jumpstat 61 | PS> $stats[10].Weight = -1 62 | PS> jumpstat -Save 63 | ``` 64 | 65 | Setting a weight to a negative number like that will cause it to be blacklisted 66 | from all jump results. Use the `jumpstat -All` option to see negative weights 67 | included in the jumpstat result set. 68 | 69 | When you remove/rename directories, Jump-Location database can become out of sync with the file system. Your top-choice will pointed to an invalid location. You can **cleanup** database with `jumpstat -cleanup`. It will remove all records with non-existing paths. 70 | 71 | Jumpstat can also be used with the "scan" parameter (jumpstat -scan) to recursively 72 | scan sub-directories into the database with 0 Weight. 73 | This is a quick way to teach Jump-Location about related directories 74 | without having to manually cd'ing to each one individually. 75 | 76 | Add all subfolders of the current directory: 77 | ``` 78 | jumpstat -scan . 79 | ``` 80 | 81 | Installation 82 | ------------ 83 | 84 | *Important:* Jump-Location requires PowerShell version 3 or higher. Older installations of Windows 7 may only have PowerShell version 2. [How to update powershell][7]. 85 | 86 | **Recommended**: Install from [psget.net][8]: 87 | ``` 88 | Install-Module Jump.Location 89 | ``` 90 | 91 | There is also a [Chocolatey package][6] for Jump-Location. To install via Chocolaty 92 | 93 | ``` 94 | choco install Jump-Location 95 | ``` 96 | 97 | Otherwise you can still install it manually. 98 | 99 | 1. Download [the latest release][5]. 100 | 2. Open properties for zip file and click "Unblock" button if you have one. 101 | 3. Unzip 102 | 4. Open a PowerShell console 103 | 5. Run `.\Install.ps1`. You may need to allow remote scripts by running 104 | `Set-ExecutionPolicy -RemoteSigned`. You may also have to right-click `Install.ps1` 105 | and Unblock it from the properties window. 106 | **Alternative:** 107 | Add line `Import-Module $modules\Jump-Location\Jump.Location.psd1` to your `$PROFILE`, 108 | where `$modules\Jump-Location` is a path to folder with module. 109 | 110 | Next time you open a PowerShell console Jump-Location will start learning 111 | your habits. You'll also have access to the `j` and `jumpstat` aliases. 112 | 113 | If you get errors after installation, try unblocking the file Jump.Location.dll manually by 114 | one of the following methods to clear the "untrusted" flag: 115 | 116 | 1. Copy the file to a FAT32 file system (such as a memory card) and back. 117 | 2. Run cmd /c "echo.>Jump.Location.dll:Zone.Identifier" 118 | 119 | If you find any bugs, please report them so I can fix them quickly! 120 | 121 | Build from source 122 | ----------------- 123 | From root directory: 124 | 125 | 1. Run `msbuild` . 126 | 2. Run `.\copyBuild.ps1` 127 | 128 | In directory `Build` you will have local build of module. 129 | 130 | TODO 131 | ---------- 132 | 1. Local search. `j . blah` will only match dirs under cwd. Using `.` will also search outside the DB. 133 | 2. Better PS documentation 134 | 135 | References 136 | ---------- 137 | 1. [old releases][4]. 138 | 139 | [1]: https://github.com/joelthelion/autojump 140 | [2]: http://stackoverflow.com/a/11813545/503826 141 | [3]: http://blogs.msdn.com/b/powershell/archive/2009/07/15/final-approved-verb-list-for-windows-powershell-2-0.aspx 142 | [4]: https://github.com/tkellogg/Jump-Location/downloads 143 | [5]: https://sourceforge.net/projects/jumplocation/files/latest/download 144 | [6]: https://chocolatey.org/packages/Jump-Location 145 | [7]: http://social.technet.microsoft.com/wiki/contents/articles/21016.how-to-install-windows-powershell-4-0.aspx 146 | [8]: http://psget.net/ 147 | -------------------------------------------------------------------------------- /.nuget/NuGet.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildProjectDirectory)\..\ 5 | 6 | 7 | false 8 | 9 | 10 | false 11 | 12 | 13 | false 14 | 15 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) 27 | $([System.IO.Path]::Combine($(ProjectDir), "packages.config")) 28 | $([System.IO.Path]::Combine($(SolutionDir), "packages")) 29 | 30 | 31 | 32 | 33 | $(SolutionDir).nuget 34 | packages.config 35 | $(SolutionDir)packages 36 | 37 | 38 | 39 | 40 | $(NuGetToolsPath)\nuget.exe 41 | @(PackageSource) 42 | 43 | "$(NuGetExePath)" 44 | mono --runtime=v4.0.30319 $(NuGetExePath) 45 | 46 | $(TargetDir.Trim('\\')) 47 | 48 | 49 | $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" -o "$(PackagesDir)" 50 | $(NuGetCommand) pack "$(ProjectPath)" -p Configuration=$(Configuration) -o "$(PackageOutputDir)" -symbols 51 | 52 | 53 | 54 | RestorePackages; 55 | $(BuildDependsOn); 56 | 57 | 58 | 59 | 60 | $(BuildDependsOn); 61 | BuildPackage; 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | 82 | 84 | 85 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /Jump.Location/CommandController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Configuration; 4 | using System.Diagnostics; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Management.Automation.Runspaces; 8 | using System.Reflection; 9 | using System.Threading; 10 | 11 | namespace Jump.Location 12 | { 13 | class CommandController 14 | { 15 | private IDatabase database; 16 | private readonly IFileStoreProvider fileStore; 17 | private bool needsToSave; 18 | // pin timer to prevent GC 19 | private Timer saveTimer; 20 | 21 | // In powershell_ise different tabs are represent different runspaces in the same process. 22 | // The prepor implementation requires ConditionalWeakTable from .NET 4.5 23 | private Dictionary _waitPeriodDictionary; 24 | 25 | private DateTime lastSaveDate = DateTime.Now; 26 | private static CommandController defaultInstance; 27 | 28 | internal CommandController(IDatabase database, IFileStoreProvider fileStore) 29 | { 30 | _waitPeriodDictionary = new Dictionary(); 31 | 32 | // This is so that we can read config settings from DLL config file 33 | string configFile = Assembly.GetExecutingAssembly().Location + ".config"; 34 | AppDomain.CurrentDomain.SetData("APP_CONFIG_FILE", configFile); 35 | ConfigurationManager.RefreshSection("appSettings"); 36 | 37 | this.database = database; 38 | this.fileStore = fileStore; 39 | // We don't want write data to disk very often. 40 | // It's fine to lose jump data for last 2 seconds. 41 | saveTimer = new Timer(SaveCallback, null, 0, 2 * 1000); 42 | } 43 | 44 | public static CommandController DefaultInstance 45 | { 46 | get 47 | { 48 | if (defaultInstance == null) 49 | { 50 | var home = Environment.GetEnvironmentVariable("USERPROFILE"); 51 | home = home ?? Path.Combine(Environment.GetEnvironmentVariable("HOMEDRIVE"), Environment.GetEnvironmentVariable("HOMEPATH")); 52 | var dbLocation = Path.Combine(home, "jump-location.txt"); 53 | defaultInstance = Create(dbLocation); 54 | } 55 | return defaultInstance; 56 | } 57 | } 58 | 59 | public static CommandController Create(string path) 60 | { 61 | var fileStore = new FileStoreProvider(path); 62 | var database = File.Exists(path) ? fileStore.Revive() : new Database(); 63 | return new CommandController(database, fileStore); 64 | } 65 | 66 | public void UpdateLocation(string fullName) 67 | { 68 | if (_waitPeriodDictionary.ContainsKey(Runspace.DefaultRunspace)) 69 | { 70 | _waitPeriodDictionary[Runspace.DefaultRunspace].CloseAndUpdate(); 71 | } 72 | 73 | var record = database.GetByFullName(fullName); 74 | _waitPeriodDictionary[Runspace.DefaultRunspace] = new DirectoryWaitPeriod(record, DateTime.Now); 75 | Save(); 76 | } 77 | 78 | public IRecord TouchRecord(string fullName) 79 | { 80 | return database.GetByFullName(fullName); 81 | } 82 | 83 | public void Save() 84 | { 85 | needsToSave = true; 86 | } 87 | 88 | private void SaveCallback(object sender) 89 | { 90 | if (needsToSave) 91 | { 92 | try 93 | { 94 | needsToSave = false; 95 | fileStore.Save(database); 96 | lastSaveDate = DateTime.Now; 97 | } 98 | catch (Exception e) 99 | { 100 | EventLog.WriteEntry("Application", string.Format("{0}\r\n{1}", e, e.StackTrace)); 101 | } 102 | } 103 | } 104 | 105 | private void ReloadIfNecessary() 106 | { 107 | if (fileStore.LastChangedDate <= lastSaveDate) return; 108 | database = fileStore.Revive(); 109 | lastSaveDate = DateTime.Now; 110 | } 111 | 112 | public IRecord FindBest(params string[] search) 113 | { 114 | return GetMatchesForSearchTerm(search).FirstOrDefault(); 115 | } 116 | 117 | public IEnumerable GetMatchesForSearchTerm(params string[] searchTerms) 118 | { 119 | List normalizedSearchTerms = new List(); 120 | foreach (var term in searchTerms) 121 | { 122 | normalizedSearchTerms.AddRange(term.Split('\\')); 123 | } 124 | return GetMatchesForNormalizedSearchTerm(normalizedSearchTerms); 125 | } 126 | 127 | private IEnumerable GetMatchesForNormalizedSearchTerm(List searchTerms) 128 | { 129 | ReloadIfNecessary(); 130 | var matches = new List(); 131 | // Hack to return everything on empty search query. 132 | if (!searchTerms.Any()) 133 | { 134 | searchTerms.Add(""); 135 | } 136 | for (var i = 0; i < searchTerms.Count(); i++) 137 | { 138 | var isLast = i == searchTerms.Count()-1; 139 | var newMatches = GetMatchesForSingleSearchTerm(searchTerms[i], isLast); 140 | matches = i == 0 ? newMatches.ToList() : matches.Intersect(newMatches).ToList(); 141 | } 142 | 143 | return matches; 144 | } 145 | 146 | private IEnumerable GetMatchesForSingleSearchTerm(string search, bool isLast) 147 | { 148 | var used = new HashSet(); 149 | search = search.ToLower(); 150 | foreach (var record in GetOrderedRecords() 151 | .Where(x => x.PathSegments.Last().StartsWith(search))) 152 | { 153 | used.Add(record.Path); 154 | yield return record; 155 | } 156 | 157 | foreach (var record in GetOrderedRecords() 158 | .Where(record => !used.Contains(record.Path)) 159 | .Where(x => x.PathSegments.Last().Contains(search))) 160 | { 161 | used.Add(record.Path); 162 | yield return record; 163 | } 164 | 165 | if (isLast) yield break; 166 | 167 | foreach (var record in GetOrderedRecords() 168 | .Where(record => !used.Contains(record.Path)) 169 | .Where(x => x.PathSegments.Any(s => s.StartsWith(search)))) 170 | { 171 | used.Add(record.Path); 172 | yield return record; 173 | } 174 | 175 | foreach (var record in GetOrderedRecords() 176 | .Where(record => !used.Contains(record.Path)) 177 | .Where(x => x.PathSegments.Any(s => s.Contains(search)))) 178 | { 179 | used.Add(record.Path); 180 | yield return record; 181 | } 182 | } 183 | 184 | public IEnumerable GetOrderedRecords(bool includeAll = false) 185 | { 186 | return from record in database.Records 187 | where record.Weight >= 0 || includeAll 188 | orderby record.Weight descending 189 | select record; 190 | } 191 | 192 | public bool RemoveRecord(IRecord record) 193 | { 194 | return database.Remove(record); 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /Jump.Location.Specs/CommandControllerSpec.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading; 4 | using Moq; 5 | using Should; 6 | using Xunit; 7 | 8 | namespace Jump.Location.Specs 9 | { 10 | public class CommandControllerSpec 11 | { 12 | public class DescribeUpdateTimes 13 | { 14 | private readonly Mock dbMock; 15 | private readonly Mock fsMock; 16 | private readonly CommandController controller; 17 | 18 | public DescribeUpdateTimes() 19 | { 20 | dbMock = new Mock(); 21 | fsMock = new Mock(); 22 | controller = new CommandController(dbMock.Object, fsMock.Object); 23 | } 24 | 25 | [Fact] 26 | public void It_saves_eventually() 27 | { 28 | var recordMock = new Mock(); 29 | recordMock.SetupAllProperties(); 30 | dbMock.Setup(x => x.GetByFullName(It.IsAny())).Returns(recordMock.Object); 31 | 32 | controller.UpdateLocation("foo"); 33 | controller.UpdateLocation("bar"); 34 | Thread.Sleep(30); 35 | fsMock.Verify(x => x.Save(dbMock.Object)); 36 | } 37 | 38 | [Fact] 39 | public void It_updates_weights() 40 | { 41 | var recordMock = new Mock(); 42 | recordMock.SetupAllProperties(); 43 | dbMock.Setup(x => x.GetByFullName("foo")).Returns(recordMock.Object); 44 | 45 | controller.UpdateLocation("foo"); 46 | Thread.Sleep(30); 47 | controller.UpdateLocation("foo"); 48 | 49 | recordMock.Verify(x => x.AddTimeSpent(It.IsAny()), Times.Once()); 50 | } 51 | } 52 | 53 | public class DescribeFindBest 54 | { 55 | private readonly Mock dbMock; 56 | private readonly Mock fsMock; 57 | private readonly CommandController controller; 58 | 59 | public DescribeFindBest() 60 | { 61 | dbMock = new Mock(); 62 | fsMock = new Mock(); 63 | controller = new CommandController(dbMock.Object, fsMock.Object); 64 | } 65 | 66 | [Fact] 67 | public void Exact_match_on_last_segment_from_a_DB_of_one() 68 | { 69 | dbMock.Setup(x => x.Records).Returns(new[] 70 | { 71 | new Record(@"FS::C:\Users\tkellogg", 10M), 72 | }); 73 | 74 | var record = controller.FindBest("tkellogg"); 75 | record.Path.ShouldEqual(@"C:\Users\tkellogg"); 76 | } 77 | 78 | [Fact] 79 | public void Exact_match_on_last_segment_from_a_DB_of_many() 80 | { 81 | dbMock.Setup(x => x.Records).Returns(new[] 82 | { 83 | new Record(@"FS::C:\Users\Kerianne", 10M), 84 | new Record(@"FS::C:\Users\lthompson", 10M), 85 | new Record(@"FS::C:\Users\tkellogg", 10M), 86 | }); 87 | 88 | var record = controller.FindBest("tkellogg"); 89 | record.Path.ShouldEqual(@"C:\Users\tkellogg"); 90 | } 91 | 92 | [Fact] 93 | public void No_match_returns_null() 94 | { 95 | dbMock.Setup(x => x.Records).Returns(new[] 96 | { 97 | new Record(@"FS::C:\Users\tkellogg", 10M), 98 | }); 99 | 100 | var record = controller.FindBest("notfound"); 101 | record.ShouldBeNull(); 102 | } 103 | 104 | [Fact] 105 | public void Exact_match_on_last_segment_is_case_insensitive() 106 | { 107 | dbMock.Setup(x => x.Records).Returns(new[] 108 | { 109 | new Record(@"FS::C:\Users\TKELLOGG", 10M), 110 | }); 111 | 112 | var record = controller.FindBest("tkellogg"); 113 | record.Path.ShouldEqual(@"C:\Users\TKELLOGG"); 114 | } 115 | 116 | [Fact] 117 | public void Last_segment_only_starts_with() 118 | { 119 | dbMock.Setup(x => x.Records).Returns(new[] 120 | { 121 | new Record(@"FS::C:\Users\tkellogg", 10M), 122 | }); 123 | 124 | var record = controller.FindBest("t"); 125 | record.Path.ShouldEqual(@"C:\Users\tkellogg"); 126 | } 127 | 128 | [Fact] 129 | public void Matches_substring_of_last_segment() 130 | { 131 | dbMock.Setup(x => x.Records).Returns(new[] 132 | { 133 | new Record(@"FS::C:\Users\tkellogg", 10M), 134 | }); 135 | 136 | var record = controller.FindBest("ell"); 137 | record.Path.ShouldEqual(@"C:\Users\tkellogg"); 138 | } 139 | 140 | #region Multiple arguments 141 | 142 | [Fact] 143 | public void Doesnt_match_only_middle_segments() 144 | { 145 | dbMock.Setup(x => x.Records).Returns(new[] 146 | { 147 | new Record(@"FS::C:\Users\tkellogg", 10M), 148 | }); 149 | 150 | var record = controller.FindBest("users"); 151 | record.ShouldBeNull(); 152 | } 153 | 154 | [Fact] 155 | public void Exact_match_of_middle_segment_and_beginning_of_last() 156 | { 157 | dbMock.Setup(x => x.Records).Returns(new[] 158 | { 159 | new Record(@"FS::C:\Users\tkellogg", 10M), 160 | }); 161 | 162 | var record = controller.FindBest("users", "tk"); 163 | record.Path.ShouldEqual(@"C:\Users\tkellogg"); 164 | } 165 | 166 | [Fact] 167 | public void Middle_segment_starts_with_search_term_and_beginning_of_last() 168 | { 169 | dbMock.Setup(x => x.Records).Returns(new[] 170 | { 171 | new Record(@"FS::C:\Users\tkellogg", 10M), 172 | }); 173 | 174 | var record = controller.FindBest("user", "tk"); 175 | record.Path.ShouldEqual(@"C:\Users\tkellogg"); 176 | } 177 | 178 | [Fact] 179 | public void Substring_of_middle_segment_and_beginning_of_last() 180 | { 181 | dbMock.Setup(x => x.Records).Returns(new[] 182 | { 183 | new Record(@"FS::C:\Users\tkellogg", 10M), 184 | }); 185 | 186 | var record = controller.FindBest("ers", "tk"); 187 | record.Path.ShouldEqual(@"C:\Users\tkellogg"); 188 | } 189 | 190 | [Fact] 191 | public void Right_match_for_two_segments() 192 | { 193 | dbMock.Setup(x => x.Records).Returns(new[] 194 | { 195 | new Record(@"FS::C:\Users\foo", 10M), 196 | new Record(@"FS::C:\Users\bar", 20M), 197 | }); 198 | 199 | var record = controller.FindBest("ers", "foo"); 200 | record.Path.ShouldEqual(@"C:\Users\foo"); 201 | } 202 | 203 | [Fact] 204 | public void Allow_column_in_search_terms() 205 | { 206 | dbMock.Setup(x => x.Records).Returns(new[] 207 | { 208 | new Record(@"FS::C:\Users\foo", 20M), 209 | new Record(@"FS::C:\Users\tkellogg", 10M), 210 | }); 211 | 212 | var record = controller.FindBest("c:", "tke"); 213 | record.Path.ShouldEqual(@"C:\Users\tkellogg"); 214 | } 215 | 216 | [Fact] 217 | public void Same_search_term_3_times_for_last_segment() 218 | { 219 | dbMock.Setup(x => x.Records).Returns(new[] 220 | { 221 | new Record(@"FS::C:\Users\foo", 10M), 222 | new Record(@"FS::C:\Users\bar", 20M), 223 | }); 224 | 225 | var record = controller.FindBest("foo", "foo", "foo"); 226 | record.Path.ShouldEqual(@"C:\Users\foo"); 227 | } 228 | 229 | [Fact] 230 | public void Same_search_term_2_times_for_midle_segment() 231 | { 232 | dbMock.Setup(x => x.Records).Returns(new[] 233 | { 234 | new Record(@"FS::C:\Users\foo", 10M), 235 | new Record(@"FS::C:\Users\bar", 20M), 236 | }); 237 | 238 | var record = controller.FindBest("Users", "Users", "oo"); 239 | record.Path.ShouldEqual(@"C:\Users\foo"); 240 | } 241 | 242 | [Fact] 243 | public void One_of_segments_is_full_path() 244 | { 245 | dbMock.Setup(x => x.Records).Returns(new[] 246 | { 247 | new Record(@"FS::C:\Users\foo", 10M), 248 | new Record(@"FS::C:\Users\bar", 20M), 249 | }); 250 | 251 | var record = controller.FindBest("ers", @"C:\Users\foo"); 252 | record.Path.ShouldEqual(@"C:\Users\foo"); 253 | } 254 | 255 | #endregion 256 | 257 | [Fact] 258 | public void Matches_are_ordered_by_weights() 259 | { 260 | dbMock.Setup(x => x.Records).Returns(new[] 261 | { 262 | new Record(@"FS::C:\Users\tkellogg", 10M), 263 | new Record(@"FS::C:\Users\tkellogg2", 13M), 264 | new Record(@"FS::C:\Users\tkellogg3", 15M), 265 | }); 266 | 267 | var record = controller.GetMatchesForSearchTerm(""); 268 | record.Select(x => x.Path).ToArray() 269 | .ShouldEqual(new[]{@"C:\Users\tkellogg3", @"C:\Users\tkellogg2", @"C:\Users\tkellogg"}); 270 | } 271 | 272 | [Fact] 273 | public void It_doesnt_include_negative_weights() 274 | { 275 | dbMock.Setup(x => x.Records).Returns(new[] 276 | { 277 | new Record(@"FS::C:\Users\foo", -10M), 278 | }); 279 | 280 | var record = controller.GetMatchesForSearchTerm("foo"); 281 | record.Any().ShouldBeFalse("Negative record should not have been matched"); 282 | } 283 | 284 | [Fact] 285 | public void It_updates_the_database_if_filesystem_is_newer() 286 | { 287 | fsMock.Setup(x => x.LastChangedDate).Returns(DateTime.Now.AddHours(1)); 288 | fsMock.Setup(x => x.Revive()).Returns(Mock.Of()); 289 | 290 | controller.FindBest(""); 291 | 292 | fsMock.VerifyAll(); 293 | } 294 | 295 | [Fact] 296 | public void Empty_query_return_most_popular() 297 | { 298 | dbMock.Setup(x => x.Records).Returns(new[] 299 | { 300 | new Record(@"FS::C:\Users\tkellogg", 10M), 301 | new Record(@"FS::C:\Users\tkellogg2", 13M), 302 | new Record(@"FS::C:\Users\tkellogg3", 15M), 303 | }); 304 | 305 | var record = controller.GetMatchesForSearchTerm(new string[] {}); 306 | record.Select(x => x.Path).ToArray() 307 | .ShouldEqual(new[] { @"C:\Users\tkellogg3", @"C:\Users\tkellogg2", @"C:\Users\tkellogg" }); 308 | } 309 | 310 | } 311 | } 312 | } 313 | --------------------------------------------------------------------------------