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 := rl.Searcher(rl.search, g.Suggestions) // 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) }