Hilbish/readline/comp-group.go

294 lines
8.1 KiB
Go
Raw Permalink Normal View History

package readline
import "strings"
// CompletionGroup - A group/category of items offered to completion, with its own
// name, descriptions and completion display format/type.
// The output, if there are multiple groups available for a given completion input,
// will look like ZSH's completion system.
type CompletionGroup struct {
Name string // If not nil, printed on top of the group's completions
Description string
// Candidates & related
Suggestions []string
Aliases map[string]string // A candidate has an alternative name (ex: --long, -l option flags)
Descriptions map[string]string // Items descriptions
DisplayType TabDisplayType // Map, list or normal
MaxLength int // Each group can be limited in the number of comps offered
// When this is true, the completion is inserted really (not virtually) without
// the trailing slash, if any. This is used when we want to complete paths.
TrimSlash bool
// PathSeparator - If you intend to write path completions, you can specify the path separator to use, depending on which OS you want completion for. By default, this will be set to the GOOS of the binary. This is also used internally for many things.
PathSeparator rune
// When this is true, we don't add a space after entering the candidate.
// Can be used for multi-stage completions, like URLS (scheme:// + host)
NoSpace bool
// For each group, we can define the min and max tab item length
MinTabItemLength int
MaxTabItemLength int
// Values used by the shell
tcPosX int
tcPosY int
tcMaxX int
tcMaxY int
tcOffset int
tcMaxLength int // Used when display is map/list, for determining message width
tcMaxLengthAlt int // Same as tcMaxLength but for SuggestionsAlt.
// true if we want to cycle through suggestions because they overflow MaxLength
allowCycle bool
// This is to say we are currently cycling through this group, for highlighting choice
isCurrent bool
}
// init - The completion group computes and sets all its values, and is then ready to work.
func (g *CompletionGroup) init(rl *Instance) {
// Details common to all displays
g.checkCycle(rl) // Based on the number of groups given to the shell, allows cycling or not
g.checkMaxLength(rl)
// Details specific to tab display modes
switch g.DisplayType {
case TabDisplayGrid:
g.initGrid(rl)
case TabDisplayMap:
g.initMap(rl)
case TabDisplayList:
g.initList(rl)
}
}
// updateTabFind - When searching through all completion groups (whether it be command history or not),
// we ask each of them to filter its own items and return the results to the shell for aggregating them.
// The rx parameter is passed, as the shell already checked that the search pattern is valid.
func (g *CompletionGroup) updateTabFind(rl *Instance) {
suggs := make([]string, 0)
// We perform filter right here, so we create a new completion group, and populate it with our results.
for i := range g.Suggestions {
if rl.regexSearch == nil { continue }
if rl.regexSearch.MatchString(g.Suggestions[i]) {
suggs = append(suggs, g.Suggestions[i])
} else if g.DisplayType == TabDisplayList && rl.regexSearch.MatchString(g.Descriptions[g.Suggestions[i]]) {
// this is a list so lets also check the descriptions
suggs = append(suggs, g.Suggestions[i])
}
}
// We overwrite the group's items, (will be refreshed as soon as something is typed in the search)
g.Suggestions = suggs
// Finally, the group computes its new printing settings
g.init(rl)
// If we are in history completion, we directly pass to the first candidate
if rl.modeAutoFind && rl.searchMode == HistoryFind && len(g.Suggestions) > 0 {
g.tcPosY = 1
}
}
// checkCycle - Based on the number of groups given to the shell, allows cycling or not
func (g *CompletionGroup) checkCycle(rl *Instance) {
if len(rl.tcGroups) == 1 {
g.allowCycle = true
}
if len(rl.tcGroups) >= 10 {
g.allowCycle = false
}
}
// checkMaxLength - Based on the number of groups given to the shell, check/set MaxLength defaults
func (g *CompletionGroup) checkMaxLength(rl *Instance) {
// This means the user forgot to set it
if g.MaxLength == 0 {
if len(rl.tcGroups) < 5 {
g.MaxLength = 20
}
if len(rl.tcGroups) >= 5 {
g.MaxLength = 20
}
// Lists that have a alternative completions are not allowed to have
// MaxLength set, because rolling does not work yet.
if g.DisplayType == TabDisplayList {
g.MaxLength = 1000 // Should be enough not to trigger anything related.
}
}
}
// checkNilItems - For each completion group we avoid nil maps and possibly other items
func checkNilItems(groups []*CompletionGroup) (checked []*CompletionGroup) {
for _, grp := range groups {
if grp.Descriptions == nil || len(grp.Descriptions) == 0 {
grp.Descriptions = make(map[string]string)
}
if grp.Aliases == nil || len(grp.Aliases) == 0 {
grp.Aliases = make(map[string]string)
}
checked = append(checked, grp)
}
return
}
// writeCompletion - This function produces a formatted string containing all appropriate items
// and according to display settings. This string is then appended to the main completion string.
func (g *CompletionGroup) writeCompletion(rl *Instance) (comp string) {
// Avoids empty groups in suggestions
if len(g.Suggestions) == 0 {
return
}
// Depending on display type we produce the approriate string
switch g.DisplayType {
case TabDisplayGrid:
comp += g.writeGrid(rl)
case TabDisplayMap:
comp += g.writeMap(rl)
case TabDisplayList:
comp += g.writeList(rl)
}
return
}
// getCurrentCell - The completion groups computes the current cell value,
// depending on its display type and its different parameters
func (g *CompletionGroup) getCurrentCell(rl *Instance) string {
switch g.DisplayType {
case TabDisplayGrid:
// x & y coodinates + safety check
cell := (g.tcMaxX * (g.tcPosY - 1)) + g.tcOffset + g.tcPosX - 1
if cell < 0 {
cell = 0
}
if cell < len(g.Suggestions) {
return g.Suggestions[cell]
}
return ""
case TabDisplayMap:
// x & y coodinates + safety check
cell := g.tcOffset + g.tcPosY - 1
if cell < 0 {
cell = 0
}
sugg := g.Suggestions[cell]
return sugg
case TabDisplayList:
// x & y coodinates + safety check
cell := g.tcOffset + g.tcPosY - 1
if cell < 0 {
cell = 0
}
sugg := g.Suggestions[cell]
// If we are in the alt suggestions column, check key and return
if g.tcPosX == 1 {
if alt, ok := g.Aliases[sugg]; ok {
return alt
}
return sugg
}
return sugg
}
// We should never get here
return ""
}
func (g *CompletionGroup) goFirstCell() {
switch g.DisplayType {
case TabDisplayGrid:
g.tcPosX = 1
g.tcPosY = 1
case TabDisplayList:
g.tcPosX = 0
g.tcPosY = 1
g.tcOffset = 0
case TabDisplayMap:
g.tcPosX = 0
g.tcPosY = 1
g.tcOffset = 0
}
}
func (g *CompletionGroup) goLastCell() {
switch g.DisplayType {
case TabDisplayGrid:
g.tcPosY = g.tcMaxY
restX := len(g.Suggestions) % g.tcMaxX
if restX != 0 {
g.tcPosX = restX
} else {
g.tcPosX = g.tcMaxX
}
// We need to adjust the X position depending
// on the interpretation of the remainder with
// respect to the group's MaxLength.
restY := len(g.Suggestions) % g.tcMaxY
maxY := len(g.Suggestions) / g.tcMaxX
if restY == 0 && maxY > g.MaxLength {
g.tcPosX = g.tcMaxX
}
if restY != 0 && maxY > g.MaxLength-1 {
g.tcPosX = g.tcMaxX
}
case TabDisplayList:
// By default, the last item is at maxY
g.tcPosY = g.tcMaxY
// If the max length is smaller than the number
// of suggestions, we need to adjust the offset.
if len(g.Suggestions) > g.MaxLength {
g.tcOffset = len(g.Suggestions) - g.tcMaxY
}
// We do not take into account the alternative suggestions
g.tcPosX = 0
case TabDisplayMap:
// By default, the last item is at maxY
g.tcPosY = g.tcMaxY
// If the max length is smaller than the number
// of suggestions, we need to adjust the offset.
if len(g.Suggestions) > g.MaxLength {
g.tcOffset = len(g.Suggestions) - g.tcMaxY
}
// We do not take into account the alternative suggestions
g.tcPosX = 0
}
}
func fmtEscape(s string) string {
return strings.Replace(s, "%", "%%", -1)
}