parent
45b1dac1bf
commit
4234a63d7d
@ -1,9 +1,10 @@
|
|||||||
package components
|
package components
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"sort"
|
||||||
|
|
||||||
"github.com/erroneousboat/termui"
|
"github.com/erroneousboat/termui"
|
||||||
|
"github.com/renstrom/fuzzysearch/fuzzy"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -21,6 +22,9 @@ type Channels struct {
|
|||||||
SelectedChannel int // index of which channel is selected from the List
|
SelectedChannel int // index of which channel is selected from the List
|
||||||
Offset int // from what offset are channels rendered
|
Offset int // from what offset are channels rendered
|
||||||
CursorPosition int // the y position of the 'cursor'
|
CursorPosition int // the y position of the 'cursor'
|
||||||
|
|
||||||
|
SearchMatches []int // index of the search matches
|
||||||
|
SearchPosition int // current position of a search match
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateChannels is the constructor for the Channels component
|
// CreateChannels is the constructor for the Channels component
|
||||||
@ -196,37 +200,70 @@ func (c *Channels) ScrollDown() {
|
|||||||
// when a match has been found the selected channel will then
|
// when a match has been found the selected channel will then
|
||||||
// be the channel that has been found
|
// be the channel that has been found
|
||||||
func (c *Channels) Search(term string) {
|
func (c *Channels) Search(term string) {
|
||||||
for i, item := range c.List.Items {
|
c.SearchMatches = make([]int, 0)
|
||||||
if strings.Contains(item, term) {
|
|
||||||
|
|
||||||
// The new position
|
matches := fuzzy.RankFind(term, c.List.Items)
|
||||||
newPos := i
|
sort.Sort(matches)
|
||||||
|
|
||||||
// Is the new position in range of the current view?
|
for _, m := range matches {
|
||||||
minRange := c.Offset
|
for i, item := range c.List.Items {
|
||||||
maxRange := c.Offset + (c.List.InnerBounds().Max.Y - 2)
|
if m.Target == item {
|
||||||
|
c.SearchMatches = append(c.SearchMatches, i)
|
||||||
if newPos < minRange {
|
break
|
||||||
// newPos is above, we need to scroll up.
|
|
||||||
c.SetSelectedChannel(i)
|
|
||||||
|
|
||||||
// How much do we need to scroll to get it into range?
|
|
||||||
c.Offset = c.Offset - (minRange - newPos)
|
|
||||||
} else if newPos > maxRange {
|
|
||||||
// newPos is below, we need to scroll down
|
|
||||||
c.SetSelectedChannel(i)
|
|
||||||
|
|
||||||
// How much do we need to scroll to get it into range?
|
|
||||||
c.Offset = c.Offset + (newPos - maxRange)
|
|
||||||
} else {
|
|
||||||
// newPos is inside range
|
|
||||||
c.SetSelectedChannel(i)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set cursor to correct position
|
|
||||||
c.CursorPosition = (newPos - c.Offset) + 1
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.GotoPosition(0)
|
||||||
|
c.SearchPosition = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GotoPosition is used by the search functionality to automatically
|
||||||
|
// scroll to a specific location in the channels component
|
||||||
|
func (c *Channels) GotoPosition(position int) {
|
||||||
|
|
||||||
|
// The new position
|
||||||
|
newPos := c.SearchMatches[position]
|
||||||
|
|
||||||
|
// Is the new position in range of the current view?
|
||||||
|
minRange := c.Offset
|
||||||
|
maxRange := c.Offset + (c.List.InnerBounds().Max.Y - 2)
|
||||||
|
|
||||||
|
if newPos < minRange {
|
||||||
|
// newPos is above, we need to scroll up.
|
||||||
|
c.SetSelectedChannel(newPos)
|
||||||
|
|
||||||
|
// How much do we need to scroll to get it into range?
|
||||||
|
c.Offset = c.Offset - (minRange - newPos)
|
||||||
|
} else if newPos > maxRange {
|
||||||
|
// newPos is below, we need to scroll down
|
||||||
|
c.SetSelectedChannel(newPos)
|
||||||
|
|
||||||
|
// How much do we need to scroll to get it into range?
|
||||||
|
c.Offset = c.Offset + (newPos - maxRange)
|
||||||
|
} else {
|
||||||
|
// newPos is inside range
|
||||||
|
c.SetSelectedChannel(newPos)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set cursor to correct position
|
||||||
|
c.CursorPosition = (newPos - c.Offset) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchNext allows us to cycle through the c.SearchMatches
|
||||||
|
func (c *Channels) SearchNext() {
|
||||||
|
newPosition := c.SearchPosition + 1
|
||||||
|
if newPosition <= len(c.SearchMatches)-1 {
|
||||||
|
c.GotoPosition(newPosition)
|
||||||
|
c.SearchPosition = newPosition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchPrev allows us to cycle through the c.SearchMatches
|
||||||
|
func (c *Channels) SearchPrev() {
|
||||||
|
newPosition := c.SearchPosition - 1
|
||||||
|
if newPosition >= 0 {
|
||||||
|
c.GotoPosition(newPosition)
|
||||||
|
c.SearchPosition = newPosition
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,8 @@ func NewConfig(filepath string) (*Config, error) {
|
|||||||
"<next>": "chat-down",
|
"<next>": "chat-down",
|
||||||
"C-f": "chat-down",
|
"C-f": "chat-down",
|
||||||
"C-d": "chat-down",
|
"C-d": "chat-down",
|
||||||
|
"n": "channel-search-next",
|
||||||
|
"N": "channel-search-prev",
|
||||||
"q": "quit",
|
"q": "quit",
|
||||||
"<f1>": "help",
|
"<f1>": "help",
|
||||||
},
|
},
|
||||||
|
@ -20,24 +20,26 @@ var timer *time.Timer
|
|||||||
// these action names can then be used to bind them to specific keys
|
// these action names can then be used to bind them to specific keys
|
||||||
// in the Config.
|
// in the Config.
|
||||||
var actionMap = map[string]func(*context.AppContext){
|
var actionMap = map[string]func(*context.AppContext){
|
||||||
"space": actionSpace,
|
"space": actionSpace,
|
||||||
"backspace": actionBackSpace,
|
"backspace": actionBackSpace,
|
||||||
"delete": actionDelete,
|
"delete": actionDelete,
|
||||||
"cursor-right": actionMoveCursorRight,
|
"cursor-right": actionMoveCursorRight,
|
||||||
"cursor-left": actionMoveCursorLeft,
|
"cursor-left": actionMoveCursorLeft,
|
||||||
"send": actionSend,
|
"send": actionSend,
|
||||||
"quit": actionQuit,
|
"quit": actionQuit,
|
||||||
"mode-insert": actionInsertMode,
|
"mode-insert": actionInsertMode,
|
||||||
"mode-command": actionCommandMode,
|
"mode-command": actionCommandMode,
|
||||||
"mode-search": actionSearchMode,
|
"mode-search": actionSearchMode,
|
||||||
"clear-input": actionClearInput,
|
"clear-input": actionClearInput,
|
||||||
"channel-up": actionMoveCursorUpChannels,
|
"channel-up": actionMoveCursorUpChannels,
|
||||||
"channel-down": actionMoveCursorDownChannels,
|
"channel-down": actionMoveCursorDownChannels,
|
||||||
"channel-top": actionMoveCursorTopChannels,
|
"channel-top": actionMoveCursorTopChannels,
|
||||||
"channel-bottom": actionMoveCursorBottomChannels,
|
"channel-bottom": actionMoveCursorBottomChannels,
|
||||||
"chat-up": actionScrollUpChat,
|
"channel-search-next": actionSearchNextChannels,
|
||||||
"chat-down": actionScrollDownChat,
|
"channel-search-prev": actionSearchPrevChannels,
|
||||||
"help": actionHelp,
|
"chat-up": actionScrollUpChat,
|
||||||
|
"chat-down": actionScrollDownChat,
|
||||||
|
"help": actionHelp,
|
||||||
}
|
}
|
||||||
|
|
||||||
func RegisterEventHandlers(ctx *context.AppContext) {
|
func RegisterEventHandlers(ctx *context.AppContext) {
|
||||||
@ -318,6 +320,16 @@ func actionMoveCursorBottomChannels(ctx *context.AppContext) {
|
|||||||
actionChangeChannel(ctx)
|
actionChangeChannel(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func actionSearchNextChannels(ctx *context.AppContext) {
|
||||||
|
ctx.View.Channels.SearchNext()
|
||||||
|
actionChangeChannel(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func actionSearchPrevChannels(ctx *context.AppContext) {
|
||||||
|
ctx.View.Channels.SearchPrev()
|
||||||
|
actionChangeChannel(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
func actionChangeChannel(ctx *context.AppContext) {
|
func actionChangeChannel(ctx *context.AppContext) {
|
||||||
// Clear messages from Chat pane
|
// Clear messages from Chat pane
|
||||||
ctx.View.Chat.ClearMessages()
|
ctx.View.Chat.ClearMessages()
|
||||||
|
21
vendor/github.com/renstrom/fuzzysearch/LICENSE
generated
vendored
Normal file
21
vendor/github.com/renstrom/fuzzysearch/LICENSE
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015 Peter Renström
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
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.
|
167
vendor/github.com/renstrom/fuzzysearch/fuzzy/fuzzy.go
generated
vendored
Normal file
167
vendor/github.com/renstrom/fuzzysearch/fuzzy/fuzzy.go
generated
vendored
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
// Fuzzy searching allows for flexibly matching a string with partial input,
|
||||||
|
// useful for filtering data very quickly based on lightweight user input.
|
||||||
|
package fuzzy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
var noop = func(r rune) rune { return r }
|
||||||
|
|
||||||
|
// Match returns true if source matches target using a fuzzy-searching
|
||||||
|
// algorithm. Note that it doesn't implement Levenshtein distance (see
|
||||||
|
// RankMatch instead), but rather a simplified version where there's no
|
||||||
|
// approximation. The method will return true only if each character in the
|
||||||
|
// source can be found in the target and occurs after the preceding matches.
|
||||||
|
func Match(source, target string) bool {
|
||||||
|
return match(source, target, noop)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchFold is a case-insensitive version of Match.
|
||||||
|
func MatchFold(source, target string) bool {
|
||||||
|
return match(source, target, unicode.ToLower)
|
||||||
|
}
|
||||||
|
|
||||||
|
func match(source, target string, fn func(rune) rune) bool {
|
||||||
|
lenDiff := len(target) - len(source)
|
||||||
|
|
||||||
|
if lenDiff < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if lenDiff == 0 && source == target {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
Outer:
|
||||||
|
for _, r1 := range source {
|
||||||
|
for i, r2 := range target {
|
||||||
|
if fn(r1) == fn(r2) {
|
||||||
|
target = target[i+utf8.RuneLen(r2):]
|
||||||
|
continue Outer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find will return a list of strings in targets that fuzzy matches source.
|
||||||
|
func Find(source string, targets []string) []string {
|
||||||
|
return find(source, targets, noop)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindFold is a case-insensitive version of Find.
|
||||||
|
func FindFold(source string, targets []string) []string {
|
||||||
|
return find(source, targets, unicode.ToLower)
|
||||||
|
}
|
||||||
|
|
||||||
|
func find(source string, targets []string, fn func(rune) rune) []string {
|
||||||
|
var matches []string
|
||||||
|
|
||||||
|
for _, target := range targets {
|
||||||
|
if match(source, target, fn) {
|
||||||
|
matches = append(matches, target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches
|
||||||
|
}
|
||||||
|
|
||||||
|
// RankMatch is similar to Match except it will measure the Levenshtein
|
||||||
|
// distance between the source and the target and return its result. If there
|
||||||
|
// was no match, it will return -1.
|
||||||
|
// Given the requirements of match, RankMatch only needs to perform a subset of
|
||||||
|
// the Levenshtein calculation, only deletions need be considered, required
|
||||||
|
// additions and substitutions would fail the match test.
|
||||||
|
func RankMatch(source, target string) int {
|
||||||
|
return rank(source, target, noop)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RankMatchFold is a case-insensitive version of RankMatch.
|
||||||
|
func RankMatchFold(source, target string) int {
|
||||||
|
return rank(source, target, unicode.ToLower)
|
||||||
|
}
|
||||||
|
|
||||||
|
func rank(source, target string, fn func(rune) rune) int {
|
||||||
|
lenDiff := len(target) - len(source)
|
||||||
|
|
||||||
|
if lenDiff < 0 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
if lenDiff == 0 && source == target {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
runeDiff := 0
|
||||||
|
|
||||||
|
Outer:
|
||||||
|
for _, r1 := range source {
|
||||||
|
for i, r2 := range target {
|
||||||
|
if fn(r1) == fn(r2) {
|
||||||
|
target = target[i+utf8.RuneLen(r2):]
|
||||||
|
continue Outer
|
||||||
|
} else {
|
||||||
|
runeDiff++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count up remaining char
|
||||||
|
for len(target) > 0 {
|
||||||
|
target = target[utf8.RuneLen(rune(target[0])):]
|
||||||
|
runeDiff++
|
||||||
|
}
|
||||||
|
|
||||||
|
return runeDiff
|
||||||
|
}
|
||||||
|
|
||||||
|
// RankFind is similar to Find, except it will also rank all matches using
|
||||||
|
// Levenshtein distance.
|
||||||
|
func RankFind(source string, targets []string) Ranks {
|
||||||
|
var r Ranks
|
||||||
|
for _, target := range find(source, targets, noop) {
|
||||||
|
distance := LevenshteinDistance(source, target)
|
||||||
|
r = append(r, Rank{source, target, distance})
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// RankFindFold is a case-insensitive version of RankFind.
|
||||||
|
func RankFindFold(source string, targets []string) Ranks {
|
||||||
|
var r Ranks
|
||||||
|
for _, target := range find(source, targets, unicode.ToLower) {
|
||||||
|
distance := LevenshteinDistance(source, target)
|
||||||
|
r = append(r, Rank{source, target, distance})
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
type Rank struct {
|
||||||
|
// Source is used as the source for matching.
|
||||||
|
Source string
|
||||||
|
|
||||||
|
// Target is the word matched against.
|
||||||
|
Target string
|
||||||
|
|
||||||
|
// Distance is the Levenshtein distance between Source and Target.
|
||||||
|
Distance int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Ranks []Rank
|
||||||
|
|
||||||
|
func (r Ranks) Len() int {
|
||||||
|
return len(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Ranks) Swap(i, j int) {
|
||||||
|
r[i], r[j] = r[j], r[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Ranks) Less(i, j int) bool {
|
||||||
|
return r[i].Distance < r[j].Distance
|
||||||
|
}
|
43
vendor/github.com/renstrom/fuzzysearch/fuzzy/levenshtein.go
generated
vendored
Normal file
43
vendor/github.com/renstrom/fuzzysearch/fuzzy/levenshtein.go
generated
vendored
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package fuzzy
|
||||||
|
|
||||||
|
// LevenshteinDistance measures the difference between two strings.
|
||||||
|
// The Levenshtein distance between two words is the minimum number of
|
||||||
|
// single-character edits (i.e. insertions, deletions or substitutions)
|
||||||
|
// required to change one word into the other.
|
||||||
|
//
|
||||||
|
// This implemention is optimized to use O(min(m,n)) space and is based on the
|
||||||
|
// optimized C version found here:
|
||||||
|
// http://en.wikibooks.org/wiki/Algorithm_implementation/Strings/Levenshtein_distance#C
|
||||||
|
func LevenshteinDistance(s, t string) int {
|
||||||
|
r1, r2 := []rune(s), []rune(t)
|
||||||
|
column := make([]int, len(r1)+1)
|
||||||
|
|
||||||
|
for y := 1; y <= len(r1); y++ {
|
||||||
|
column[y] = y
|
||||||
|
}
|
||||||
|
|
||||||
|
for x := 1; x <= len(r2); x++ {
|
||||||
|
column[0] = x
|
||||||
|
|
||||||
|
for y, lastDiag := 1, x-1; y <= len(r1); y++ {
|
||||||
|
oldDiag := column[y]
|
||||||
|
cost := 0
|
||||||
|
if r1[y-1] != r2[x-1] {
|
||||||
|
cost = 1
|
||||||
|
}
|
||||||
|
column[y] = min(column[y]+1, column[y-1]+1, lastDiag+cost)
|
||||||
|
lastDiag = oldDiag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return column[len(r1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
func min(a, b, c int) int {
|
||||||
|
if a < b && a < c {
|
||||||
|
return a
|
||||||
|
} else if b < c {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
6
vendor/vendor.json
vendored
6
vendor/vendor.json
vendored
@ -90,6 +90,12 @@
|
|||||||
"revision": "aa4a75b1c20a2b03751b1a9f7e41d58bd6f71c43",
|
"revision": "aa4a75b1c20a2b03751b1a9f7e41d58bd6f71c43",
|
||||||
"revisionTime": "2017-11-04T16:23:16Z"
|
"revisionTime": "2017-11-04T16:23:16Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "DF3jZEw4lCq/SEaC7DIl/R+7S70=",
|
||||||
|
"path": "github.com/renstrom/fuzzysearch/fuzzy",
|
||||||
|
"revision": "2d205ac6ec17a839a94bdbfd16d2fa6c6dada2e0",
|
||||||
|
"revisionTime": "2016-03-31T20:48:55Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "go/ast",
|
"path": "go/ast",
|
||||||
"revision": ""
|
"revision": ""
|
||||||
|
Loading…
Reference in New Issue
Block a user