From 18b216ca27b31f92b52f509ece2039119937e897 Mon Sep 17 00:00:00 2001 From: Brian Hicks Date: Wed, 3 Jun 2020 08:37:01 -0500 Subject: [PATCH] import from my dotfiles --- LICENSE | 4 +++ README.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++++ default.nix | 15 +++++++++++ similar-sort.go | 68 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 155 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 default.nix create mode 100644 similar-sort.go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..704a88d --- /dev/null +++ b/LICENSE @@ -0,0 +1,4 @@ +similar-sort is licensed CC BY-SA 4.0. + +"Wait, why?", I hear you say, "That's an unusual choice for source code!" +Put quite simply, it's because this package is a wrapper around a Levenshtein distance implementation I got from WikiPedia, which is licensed under CC BY-SA 3.0. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a5e6b6e --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# Similar Sort + +This is a small Go program that will: + +1. take a reference string as the first argument +2. and a list of candidate strings in stdin +3. and output the candidates sorted according to their edit distance from the reference, lowest first. + +"What use is this?" you may ask! +Well! +It turns out to be really useful to do fuzzy file finding a large project. + +When I am in some filesystem hierarchy and I trigger my fuzzy-finder, I want to see sibling files before I see similarly-named files further away. +I also want to match on test files pretty easily. +Say I have this project structure: + +``` +example +└── src +    ├── Main.elm +    └── Page +    └── Learn +    └── Home +    ├── Main.elm +    └── View.elm +``` + +If I am in `src/Page/Learn/Home/View.elm` and I want to get to the sibling file `Main.elm`, the default `fzf` config shows me `src/Main.elm` first. +That's not what I wanted! + +But if I sort the files instead by passing them through `similar-sort src/Page/Learn/Home/View.elm`, the sibling file will show up first. +This works surprisingly well, and I really like it! + +It could probably perform a *little* better by doing some heuristic based on equivalent file structure except for the addition/removal of "tests", "specs", etc, but I haven't bothered yet. + +## Installing + +You can look in `dotfiles/kakoune.nix` in the root of this project to see how to use this in a home-manager context. +If you're not using home-manager, or you just want to install it globally, `cd` here and type: + +```sh +nix-env -if . +``` + +OR if you have `go` installed but not `nix`, just `go build similar-sort.go`; it has no external dependencies and will result in a static binary you can put wherever. + +### Adding to Vim + +Add this to your vim config: + +```vim +nnoremap :call fzf#run(fzf#wrap({ + \ "source": "git ls-files --others --cached --exclude-standard \| similar-sort " . @% . " \| grep -v " . @%, + \ "sink": "edit", + \ "options": "--tiebreak index" + \ })) +``` + +(You'll need `fzf` and `fzf.vim` installed.) +This will bind ctrl-t to the fuzzy finder. +When you select a match, it will open in the current pane. + +If you want to split or vsplit, change `"sink": "edit"` to `"sink": "split"` or `"sink": "vsplit"`. +See the docs for `fzf#run` for more customization options. + +### Adding to Kakoune + +I use [connect.kak](https://github.com/alexherbo2/connect.kak) to spawn a terminal window with about the same command line as in the vim config above. diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..1a1dc76 --- /dev/null +++ b/default.nix @@ -0,0 +1,15 @@ +{ pkgs ? import { }, ... }: +pkgs.stdenv.mkDerivation { + name = "similar-sort"; + buildInputs = [ pkgs.go ]; + src = ./.; + + buildPhase = '' + env HOME=$(pwd) GOPATH=$(pwd) go build similar-sort.go + ''; + + installPhase = '' + mkdir -p $out/bin + cp similar-sort $out/bin + ''; +} diff --git a/similar-sort.go b/similar-sort.go new file mode 100644 index 0000000..41b0736 --- /dev/null +++ b/similar-sort.go @@ -0,0 +1,68 @@ +package main + +import "bufio" +import "fmt" +import "os" +import "sort" +import "strings" +import "unicode/utf8" + +func main() { + target := strings.Join(os.Args[1:], " ") + + s := bufio.NewScanner(os.Stdin) + var lines []WithDistance + for s.Scan() { + lines = append(lines, WithDistance{s.Text(), Levenshtein(target, s.Text())}) + } + + sort.Slice(lines, func(i, j int) bool { + return lines[i].distance < lines[j].distance + }) + + for _, line := range lines { + fmt.Println(line.text) + } +} + +type WithDistance struct { + text string + distance int +} + +// https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#Go +// made available under CC BY-SA 3.0 https://creativecommons.org/licenses/by-sa/3.0/ +func Levenshtein(a, b string) int { + f := make([]int, utf8.RuneCountInString(b)+1) + + for j := range f { + f[j] = j + } + + for _, ca := range a { + j := 1 + fj1 := f[0] // fj1 is the value of f[j - 1] in last iteration + f[0]++ + for _, cb := range b { + mn := min(f[j]+1, f[j-1]+1) // delete & insert + if cb != ca { + mn = min(mn, fj1+1) // change + } else { + mn = min(mn, fj1) // matched + } + + fj1, f[j] = f[j], mn // save f[j] to fj1(j is about to increase), update f[j] to mn + j++ + } + } + + return f[len(f)-1] +} + +func min(a, b int) int { + if a <= b { + return a + } else { + return b + } +}