Hilbish/readline/tab-virtual.go

291 lines
8.2 KiB
Go

package readline
import (
"strings"
"github.com/rivo/uniseg"
)
// insertCandidateVirtual - When a completion candidate is selected, we insert it virtually in the input line:
// this will not trigger further firltering against the other candidates. Each time this function
// is called, any previous candidate is dropped, after being used for moving the cursor around.
func (rl *Instance) insertCandidateVirtual(candidate []rune) {
for {
// I don't really understand why `0` is creaping in at the end of the
// array but it only happens with unicode characters.
if len(candidate) > 1 && candidate[len(candidate)-1] == 0 {
candidate = candidate[:len(candidate)-1]
continue
}
break
}
// We place the cursor back at the beginning of the previous virtual candidate
rl.pos -= len(rl.currentComp)
// We delete the previous virtual completion, just
// like we would delete a word in vim editing mode.
if len(rl.currentComp) == 1 {
rl.deleteVirtual() // Delete a single character
} else if len(rl.currentComp) > 0 {
rl.viDeleteByAdjustVirtual(rl.viJumpEVirtual(tokeniseSplitSpaces) + 1)
}
// We then keep a reference to the new candidate
rl.currentComp = candidate
// We should not have a remaining virtual completion
// line, so it is now identical to the real line.
rl.lineComp = rl.line
// Insert the new candidate in the virtual line.
switch {
case len(rl.lineComp) == 0:
rl.lineComp = candidate
case rl.pos == 0:
rl.lineComp = append(candidate, rl.lineComp...)
case rl.pos < len(rl.lineComp):
r := append(candidate, rl.lineComp[rl.pos:]...)
rl.lineComp = append(rl.lineComp[:rl.pos], r...)
default:
rl.lineComp = append(rl.lineComp, candidate...)
}
// We place the cursor at the end of our new virtually completed item
rl.pos += len(candidate)
}
// Insert the current completion candidate into the input line.
// This candidate might either be the currently selected one (white frame),
// or the only candidate available, if the total number of candidates is 1.
func (rl *Instance) insertCandidate() {
cur := rl.getCurrentGroup()
if cur != nil {
completion := cur.getCurrentCell(rl)
prefix := len(rl.tcPrefix)
// Special case for the only special escape, which
// if not handled, will make us insert the first
// character of our actual rl.tcPrefix in the candidate.
if strings.HasPrefix(string(rl.tcPrefix), "%") {
prefix++
}
// Ensure no indexing error happens with prefix
if len(completion) >= prefix {
rl.viDeleteByAdjust(-prefix)
rl.insert([]rune(completion))
if !cur.TrimSlash && !cur.NoSpace {
rl.insert([]rune(" "))
}
}
}
}
// updateVirtualComp - Either insert the current completion candidate virtually, or on the real line.
func (rl *Instance) updateVirtualComp() {
cur := rl.getCurrentGroup()
if cur != nil {
completion := cur.getCurrentCell(rl)
prefix := len(rl.tcPrefix)
// If the total number of completions is one, automatically insert it.
if rl.hasOneCandidate() {
rl.insertCandidate()
// Quit the tab completion mode to avoid asking to the user to press
// Enter twice to actually run the command
// Refresh first, and then quit the completion mode
rl.viUndoSkipAppend = true
rl.resetTabCompletion()
} else {
if strings.HasSuffix(string(rl.line), rl.tcPrefix) && (!rl.modeAutoFind || rl.searchMode != HistoryFind) {
rl.viDeleteByAdjust(-prefix)
}
// Special case for the only special escape, which
// if not handled, will make us insert the first
// character of our actual rl.tcPrefix in the candidate.
if strings.HasPrefix(string(rl.tcPrefix), "%") {
prefix++
}
// Or insert it virtually.
if len(completion) >= prefix {
comp := completion
if rl.modeAutoFind && rl.searchMode == HistoryFind {
comp = completion[prefix:]
}
rl.insertCandidateVirtual([]rune(comp))
}
}
}
}
// resetVirtualComp - This function is called before most of our readline key handlers,
// and makes sure that the current completion (virtually inserted) is either inserted or dropped,
// and that all related parameters are reinitialized.
func (rl *Instance) resetVirtualComp(drop bool) {
// If we don't have a current virtual completion, there's nothing to do.
// IMPORTANT: this MUST be first, to avoid nil problems with empty comps.
if len(rl.currentComp) == 0 {
return
}
// Get the current candidate and its group.
//It contains info on how we must process it
cur := rl.getCurrentGroup()
if cur == nil {
return
}
completion := cur.getCurrentCell(rl)
// Avoid problems with empty completions
if completion == "" {
rl.clearVirtualComp()
return
}
// We will only insert the net difference between prefix and completion.
prefix := len(rl.tcPrefix)
// Special case for the only special escape, which
// if not handled, will make us insert the first
// character of our actual rl.tcPrefix in the candidate.
if strings.HasPrefix(string(rl.tcPrefix), "%") {
prefix++
}
// If we are asked to drop the completion, move it away from the line and return.
if drop {
rl.pos -= len([]rune(completion[prefix:]))
rl.lineComp = rl.line
rl.clearVirtualComp()
return
}
// Insert the current candidate. A bit of processing happens:
// - We trim the trailing slash if needed
// - We add a space only if the group has not explicitely specified not to add one.
if cur.TrimSlash {
trimmed, hadSlash := trimTrailing(completion)
if !hadSlash && rl.compAddSpace {
if !cur.NoSpace {
trimmed = trimmed + " "
}
}
rl.insertCandidateVirtual([]rune(trimmed[prefix:]))
} else {
if rl.compAddSpace {
if !cur.NoSpace {
completion = completion + " "
}
}
rl.insertCandidateVirtual([]rune(completion))
}
// Reset virtual
rl.clearVirtualComp()
}
// trimTrailing - When the group to which the current candidate
// belongs has TrimSlash enabled, we process the candidate.
// This is used when the completions are directory/file paths.
func trimTrailing(comp string) (trimmed string, hadSlash bool) {
// Unix paths
if strings.HasSuffix(comp, "/") {
return strings.TrimSuffix(comp, "/"), true
}
// Windows paths
if strings.HasSuffix(comp, "\\") {
return strings.TrimSuffix(comp, "\\"), true
}
return comp, false
}
// viDeleteByAdjustVirtual - Same as viDeleteByAdjust, but for our virtually completed input line.
func (rl *Instance) viDeleteByAdjustVirtual(adjust int) {
var (
newLine []rune
backOne bool
)
// Avoid doing anything if input line is empty.
if len(rl.lineComp) == 0 {
return
}
switch {
case adjust == 0:
rl.viUndoSkipAppend = true
return
case rl.pos+adjust == len(rl.lineComp)-1:
newLine = rl.lineComp[:rl.pos]
// backOne = true // Deleted, otherwise the completion moves back when we don't want to.
case rl.pos+adjust == 0:
newLine = rl.lineComp[rl.pos:]
case adjust < 0:
newLine = append(rl.lineComp[:rl.pos+adjust], rl.lineComp[rl.pos:]...)
default:
newLine = append(rl.lineComp[:rl.pos], rl.lineComp[rl.pos+adjust:]...)
}
// We have our new line completed
rl.lineComp = newLine
if adjust < 0 {
rl.moveCursorByAdjust(adjust)
}
if backOne {
rl.pos--
}
}
// viJumpEVirtual - Same as viJumpE, but for our virtually completed input line.
func (rl *Instance) viJumpEVirtual(tokeniser func([]rune, int) ([]string, int, int)) (adjust int) {
split, index, pos := tokeniser(rl.lineComp, rl.pos)
if len(split) == 0 {
return
}
word := rTrimWhiteSpace(split[index])
switch {
case len(split) == 0:
return
case index == len(split)-1 && pos >= len(word)-1:
return
case pos >= len(word)-1:
word = rTrimWhiteSpace(split[index+1])
adjust = uniseg.GraphemeClusterCount(split[index]) - pos
adjust += uniseg.GraphemeClusterCount(word) - 1
default:
adjust = uniseg.GraphemeClusterCount(word) - pos - 1
}
return
}
func (rl *Instance) deleteVirtual() {
switch {
case len(rl.lineComp) == 0:
return
case rl.pos == 0:
rl.lineComp = rl.lineComp[1:]
case rl.pos > len(rl.lineComp):
case rl.pos == len(rl.lineComp):
rl.lineComp = rl.lineComp[:rl.pos]
default:
rl.lineComp = append(rl.lineComp[:rl.pos], rl.lineComp[rl.pos+1:]...)
}
}
// We are done with the current virtual completion candidate.
// Get ready for the next one
func (rl *Instance) clearVirtualComp() {
rl.line = rl.lineComp
rl.currentComp = []rune{}
rl.compAddSpace = false
}