Corrections + ajout de la possibilité de naviguer dans la complétion avec les touches fléchées

This commit is contained in:
Benjamin VAUDOUR 2024-03-02 21:19:12 +01:00
parent 8694cb0d18
commit bea7084d5b
4 changed files with 194 additions and 78 deletions

View File

@ -51,28 +51,30 @@ func main() {
## Raccourcis clavier <u>disponibles</u> ## Raccourcis clavier <u>disponibles</u>
| Raccourci clavier | Action | | Raccourci clavier | Action |
| -------------------------------------- | ------------------------------------------------------------ | | ----------------------------------------- | -------------------------------------------------------------------------------------------------- |
| Ctrl+A, Home | Retour au début de la saisie | | Ctrl+A, Home | Retour au début de la saisie |
| Ctrl+E, End | Retour à la fin de la saisie | | Ctrl+E, End | Retour à la fin de la saisie |
| Ctrl+B, Left | Déplacement dun caractère vers la gauche | | Ctrl+B, Left | Déplacement dun caractère vers la gauche |
| Ctrl+F, Right | Déplacement dun caractère vers la droite | | Ctrl+F, Right | Déplacement dun caractère vers la droite |
| Alt+B, Ctrl+Left | Déplacement dun mot vers la gauche | | Alt+B, Ctrl+Left | Déplacement dun mot vers la gauche |
| Alt+F, Ctrl+Right | Déplacement dun mot vers la droite | | Alt+F, Ctrl+Right | Déplacement dun mot vers la droite |
| Ctrl+U | Suppression de tous les caractères du début de la ligne au curseur (curseur non compris) | | Ctrl+U | Suppression de tous les caractères du début de la ligne au curseur (curseur non compris) |
| Ctrl+K | Suppression de tous les carctères du curseur (compris) à la fin de la ligne | | Ctrl+K | Suppression de tous les carctères du curseur (compris) à la fin de la ligne |
| Ctrl+D (si saisie commencée), Del | Suppression du caractère sous le curseur | | Ctrl+D (si saisie commencée), Del | Suppression du caractère sous le curseur |
| Ctrl+D (si ligne vide) | Termine lapplication (EOF) | | Ctrl+D (si ligne vide) | Termine lapplication (EOF) |
| Ctrl+H, Backspace | Suppression du caractère avant le curseur | | Ctrl+H, Backspace | Suppression du caractère avant le curseur |
| Alt+D, Alt+Del | Suppression du prochain mot | | Alt+D, Alt+Del | Suppression du prochain mot |
| Alt+Backspace | Suppression du précédent mot | | Alt+Backspace | Suppression du précédent mot |
| Ctrl+C | Termine lapplication (avec erreur) | | Ctrl+C | Termine lapplication (avec erreur) |
| Ctrl+P, Up (si ligne vide au départ) | Remonte à lhistorique précédent | | Ctrl+P, Up (si ligne vide au départ) | Remonte à lhistorique précédent |
| Ctrl+N, Down (si ligne vide au départ) | Descend dans lhistorique | | Ctrl+N, Down (si ligne vide au départ) | Descend dans lhistorique |
| Ctrl+R, Up (si ligne non vide) | Recherche dans lhistorique (par préfixe de ligne) à partir de la fin | | Ctrl+R, Up (si ligne non vide) | Recherche dans lhistorique (par préfixe de ligne) à partir de la fin |
| Ctrl+S, Down (si ligne non vide) | Recherche dans lhistorique (par préfixe de ligne) à partir du début | | Ctrl+S, Down (si ligne non vide) | Recherche dans lhistorique (par préfixe de ligne) à partir du début |
| Ctrl+Y | Copie le dernier élément supprimé | | Ctrl+Y | Copie le dernier élément supprimé |
| Alt+Y | Remonte dans lhistorique des éléments supprimés et les copie (implique Ctrl+Y précédemment lancé) | | Alt+Y | Remonte dans lhistorique des éléments supprimés et les copie (implique Ctrl+Y précédemment lancé) |
| Ctrl+G, Cancel | Annule les dernières actions temporaires (historique, recherche, copie, historique de copie) | | Ctrl+G, Cancel | Annule les dernières actions temporaires (historique, recherche, copie, historique de copie) |
| Tab | Complétion suivante | | Tab, Up (si complétion commencée) | Complétion suivante |
| Shift+Tab | Complétion précédente | | Shift+Tab, Down (si complétion commencée) | Complétion précédente |
| Left (si complétion commencée) | Complétion à gauche |
| Right (si complétion commencée) | Complétion à droite |

View File

@ -11,20 +11,28 @@ import (
) )
func coordToIndex(x, y, c, n int) int { func coordToIndex(x, y, c, n int) int {
if x < 0 || x >= c || y < 0 {
return -1
}
l, r := n/c, n%c l, r := n/c, n%c
switch { switch {
case (x < r && y >= l+1) || (x >= r && y >= l):
return -1
case r == 0: case r == 0:
return x*l + y return x*l + y
case x < r: case x < r:
return x*(l+1) + y return x*(l+1) + y
case y >= l:
return -1
default: default:
return r*(l+1) + (x-r)*l + y return r*(l+1) + (x-r)*l + y
} }
} }
func indexToCoord(i, c, n int) (x, y int) { func indexToCoord(i, c, n int) (x, y int) {
if i < 0 || i >= n {
return -1, -1
}
l, r := n/c, n%c l, r := n/c, n%c
switch { switch {
case r == 0: case r == 0:
@ -32,7 +40,7 @@ func indexToCoord(i, c, n int) (x, y int) {
case i < r*(l+1): case i < r*(l+1):
x, y = i/(l+1), i%(l+1) x, y = i/(l+1), i%(l+1)
default: default:
j := i - r*l j := i - r*(l+1)
x, y = r+j/l, j%l x, y = r+j/l, j%l
} }
@ -144,50 +152,118 @@ func (c *completer) next() bool { return c.choices.Next() && c.set() }
func (c *completer) move(index int) bool { return c.choices.SetCursor(index) && c.set() } func (c *completer) move(index int) bool { return c.choices.SetCursor(index) && c.set() }
func (c *completer) formatChoices(currentWord string, terminalWidth, terminalHeight int) string { func (c *completer) gridInfo(terminalWidth, terminalHeight int) (nbChoices, colWidth, nbCols, nbLines int, choices []string) {
var buf strings.Builder nbChoices = c.choices.Len()
l, cursor := c.choices.Len(), c.choices.Cursor() if nbChoices == 0 {
if l == 0 { return
return ""
} }
var maxLen int var wordWidth int
choices := make([]string, l) choices = make([]string, nbChoices)
for i := 0; i < l; i++ { for i := 0; i < nbChoices; i++ {
word, _ := c.choices.Index(i).Get() word, _ := c.choices.Index(i).Get()
maxLen, choices[i] = max(atom.VisibleWidth(word)+1, maxLen), word choices[i], wordWidth = word, max(atom.VisibleWidth(word)+1, wordWidth)
} }
maxCol := min(max(terminalWidth/maxLen, 1), 5) nbCols = min(max(terminalWidth/wordWidth, 1), 5)
lines := l / maxCol nbLines = nbChoices / nbCols
if l%maxCol > 0 { if nbChoices%nbCols > 0 {
lines++ nbLines++
} }
lineMin, lineMax := 0, lines-1
var isPartial bool colWidth = terminalWidth / nbCols
if isPartial = terminalHeight < lines; isPartial { if colWidth > wordWidth {
_, cursorLine := indexToCoord(cursor, maxCol, l) colWidth = (colWidth + wordWidth) >> 1
if cursorLine < terminalHeight { }
lineMax = min(terminalHeight, lines) - 1
return
}
func (c *completer) infoMove(terminalWidth, terminalHeight int) (nbCols, nbLines, nbChoices, x, y, r int) {
nbChoices, _, nbCols, nbLines, _ = c.gridInfo(terminalWidth, terminalHeight)
if nbChoices > 0 {
i := c.choices.Cursor()
if i < 0 || i >= nbChoices {
x, y = -1, -1
} else { } else {
lineMin, lineMax = cursorLine+1-min(terminalHeight, lines), cursorLine x, y = indexToCoord(i, nbCols, nbChoices)
r = nbChoices % nbCols
} }
} }
colWith := terminalWidth / maxCol return
if colWith > maxLen { }
colWith = (colWith + maxLen) >> 1
func (c *completer) left(terminalWidth, terminalHeight int) bool {
nbCols, nbLines, nbChoices, x, y, r := c.infoMove(terminalWidth, terminalHeight)
if nbChoices == 0 || x < 0 {
return false
} }
switch {
case x > 0:
x--
case y > 0:
x, y = nbCols-1, y-1
case r > 0:
x, y = r-1, nbLines-1
default:
x, y = nbCols-1, nbLines-1
}
i := coordToIndex(x, y, nbCols, nbChoices)
return i >= 0 && c.move(i)
}
func (c *completer) right(terminalWidth, terminalHeight int) bool {
nbCols, nbLines, nbChoices, x, y, r := c.infoMove(terminalWidth, terminalHeight)
if nbChoices == 0 || x < 0 {
return false
}
fmt.Println("debug0:", x, y)
if x < r-1 || (x < nbCols-1 && (r == 0 || y < nbLines-1)) {
x++
} else if y < nbLines-1 {
x, y = 0, y+1
} else {
x, y = 0, 0
}
fmt.Println("debug1:", x, y)
i := coordToIndex(x, y, nbCols, nbChoices)
return i >= 0 && c.move(i)
}
func (c *completer) formatChoices(currentWord string, terminalWidth, terminalHeight int) string {
nbChoices, colWidth, nbCols, nbLines, choices := c.gridInfo(terminalWidth, terminalHeight)
if nbChoices == 0 {
return ""
}
cursor := c.choices.Cursor()
lineMin, lineMax := 0, nbLines-1
var isPartial bool
if isPartial = terminalHeight < nbLines; isPartial {
_, cursorLine := indexToCoord(cursor, nbCols, nbChoices)
lineMin = max(0, cursorLine-terminalHeight+1)
lineMax = lineMin + terminalHeight - 1
}
var buf strings.Builder
for y := lineMin; y <= lineMax; y++ { for y := lineMin; y <= lineMax; y++ {
if y > 0 { if y > 0 {
buf.WriteRune('\n') buf.WriteRune('\n')
} }
for x := 0; x < maxCol; x++ { for x := 0; x < nbCols; x++ {
i := coordToIndex(x, y, maxCol, l) i := coordToIndex(x, y, nbCols, nbChoices)
if i >= 0 { if i >= 0 {
word := choices[i] word := choices[i]
realSize, displaySize := len([]rune(word)), atom.VisibleWidth(word) realSize, displaySize := len([]rune(word)), atom.VisibleWidth(word)
size := maxLen + realSize - displaySize size := colWidth + realSize - displaySize
word = strings.TrimPrefix(format.Left(word, size), currentWord) word = strings.TrimPrefix(format.Left(word, size), currentWord)
if i == cursor { if i == cursor {
buf.WriteString(format.Apply(currentWord, "bg_l_white", "black", "bold", "underline")) buf.WriteString(format.Apply(currentWord, "bg_l_white", "black", "bold", "underline"))
@ -201,7 +277,7 @@ func (c *completer) formatChoices(currentWord string, terminalWidth, terminalHei
} }
if isPartial { if isPartial {
position := fmt.Sprintf("lignes %d à %d (%d)", lineMin+1, lineMax+1, lines) position := fmt.Sprintf("lignes %d à %d (%d)", lineMin+1, lineMax+1, nbLines)
buf.WriteRune('\n') buf.WriteRune('\n')
buf.WriteString(format.Apply(position, "bg_l_cyan", "black")) buf.WriteString(format.Apply(position, "bg_l_cyan", "black"))
} }

View File

@ -113,14 +113,45 @@ func (fl *Fishline) completionNext() (ok bool) {
return return
} }
func (fl *Fishline) fixState() bool { func (fl *Fishline) getCompletionInfos(buffer ...string) (w, h int) {
ok1 := fl.st.RemoveSavedBuffer() var str string
ok2 := fl.st.RemoveHistorySearch() if len(buffer) > 0 {
ok3 := fl.st.UnfocusHistory() str = buffer[0]
ok4 := fl.st.UnfocusYank() } else {
ok5 := fl.removeCompletion() str, _ = fl.st.Buffer()
}
return ok1 || ok2 || ok3 || ok4 || ok5 w, h = fl.st.Width(), fl.st.Height()-1
hb, _, _ := atom.CursorOffset(fl.st.GetPrompt()+str, 0, 0)
return w, h - hb
}
func (fl *Fishline) completionLeft() (ok bool) {
completion := fl.initCompletion()
if ok = completion.left(fl.getCompletionInfos()); ok {
fl.setCompletion(completion)
}
return
}
func (fl *Fishline) completionRight() (ok bool) {
completion := fl.initCompletion()
if ok = completion.right(fl.getCompletionInfos()); ok {
fl.setCompletion(completion)
}
return
}
func (fl *Fishline) fixState() bool {
ok1 := fl.st.FixState()
ok2 := fl.removeCompletion()
return ok1 || ok2
} }
func (fl *Fishline) cancel() (ok bool) { func (fl *Fishline) cancel() (ok bool) {
@ -286,7 +317,9 @@ func (fl *Fishline) execSequence(s atom.Sequence, isPassword bool) (fix, stop bo
if !isPassword { if !isPassword {
l := fl.st.BufferLen() l := fl.st.BufferLen()
changeProposal = -1 changeProposal = -1
if l == 0 || fl.st.IsHistoryMode() { if fl.isCompletionMode() {
noBeep = fl.completionPrev()
} else if l == 0 || fl.st.IsHistoryMode() {
noBeep = fl.historyPrev() noBeep = fl.historyPrev()
} else { } else {
noBeep = fl.searchHistoryPrev() noBeep = fl.searchHistoryPrev()
@ -296,20 +329,30 @@ func (fl *Fishline) execSequence(s atom.Sequence, isPassword bool) (fix, stop bo
if !isPassword { if !isPassword {
l := fl.st.BufferLen() l := fl.st.BufferLen()
changeProposal = -1 changeProposal = -1
if l == 0 || fl.st.IsHistoryMode() { if fl.isCompletionMode() {
noBeep = fl.completionNext()
} else if l == 0 || fl.st.IsHistoryMode() {
noBeep = fl.historyNext() noBeep = fl.historyNext()
} else { } else {
noBeep = fl.searchHistoryNext() noBeep = fl.searchHistoryNext()
} }
} }
case atom.Right: // Move right case atom.Right: // Move right
noBeep, fix = fl.st.Right(), true if fl.isCompletionMode() && !isPassword {
if !noBeep && fl.proposal != "" { changeProposal, noBeep = -1, fl.completionRight()
noBeep, changeProposal = true, -1 } else {
fl.st.SetBuffer(fl.proposal) noBeep, fix = fl.st.Right(), true
if !noBeep && fl.proposal != "" {
noBeep, changeProposal = true, -1
fl.st.SetBuffer(fl.proposal)
}
} }
case atom.Left: // Move left case atom.Left: // Move left
noBeep, fix = fl.st.Left(), true if fl.isCompletionMode() && !isPassword {
changeProposal, noBeep = -1, fl.completionLeft()
} else {
noBeep, fix = fl.st.Left(), true
}
case atom.Ins: // Toggle insert case atom.Ins: // Toggle insert
fl.st.ToggleReplace() fl.st.ToggleReplace()
noBeep = true noBeep = true
@ -387,16 +430,11 @@ func (fl *Fishline) prompt(p string, isPassword bool) (result option.Result[stri
output.WriteString(format.Apply(suffix, "l_black")) output.WriteString(format.Apply(suffix, "l_black"))
} }
if c, ok := fl.completion.Get(); ok { if c, ok := fl.completion.Get(); ok {
w, h := fl.st.Width(), fl.st.Height()-1 w, h := fl.getCompletionInfos(buffer + suffix)
wb := atom.VisibleWidth(fl.st.GetPrompt() + buffer + suffix) choices := c.formatChoices(fl.currentWord, w, h)
hb := wb / w
if wb%w > 0 {
hb++
}
choices := c.formatChoices(fl.currentWord, w, h-hb)
if choices != "" { if choices != "" {
output.WriteString("\n") output.WriteString("\n")
output.WriteString(c.formatChoices(fl.currentWord, w, h-hb)) output.WriteString(choices)
} }
} }
} }

2
go.mod
View File

@ -1,4 +1,4 @@
module gitea.zaclys.com/bvaudour/fishline module gitea.zaclys.com/bvaudour/gfishline
go 1.22.0 go 1.22.0