diff --git a/README.md b/README.md index 56322e5..5a50452 100644 --- a/README.md +++ b/README.md @@ -51,28 +51,30 @@ func main() { ## Raccourcis clavier disponibles -| Raccourci clavier | Action | -| -------------------------------------- | ------------------------------------------------------------ | -| Ctrl+A, Home | Retour au début de la saisie | -| Ctrl+E, End | Retour à la fin de la saisie | -| Ctrl+B, Left | Déplacement d’un caractère vers la gauche | -| Ctrl+F, Right | Déplacement d’un caractère vers la droite | -| Alt+B, Ctrl+Left | Déplacement d’un mot vers la gauche | -| Alt+F, Ctrl+Right | Déplacement d’un mot vers la droite | -| 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+D (si saisie commencée), Del | Suppression du caractère sous le curseur | -| Ctrl+D (si ligne vide) | Termine l’application (EOF) | -| Ctrl+H, Backspace | Suppression du caractère avant le curseur | -| Alt+D, Alt+Del | Suppression du prochain mot | -| Alt+Backspace | Suppression du précédent mot | -| Ctrl+C | Termine l’application (avec erreur) | -| Ctrl+P, Up (si ligne vide au départ) | Remonte à l’historique précédent | -| Ctrl+N, Down (si ligne vide au départ) | Descend dans l’historique | -| Ctrl+R, Up (si ligne non vide) | Recherche dans l’historique (par préfixe de ligne) à partir de la fin | -| Ctrl+S, Down (si ligne non vide) | Recherche dans l’historique (par préfixe de ligne) à partir du début | -| Ctrl+Y | Copie le dernier élément supprimé | -| Alt+Y | Remonte dans l’historique 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) | -| Tab | Complétion suivante | -| Shift+Tab | Complétion précédente | +| Raccourci clavier | Action | +| ----------------------------------------- | -------------------------------------------------------------------------------------------------- | +| Ctrl+A, Home | Retour au début de la saisie | +| Ctrl+E, End | Retour à la fin de la saisie | +| Ctrl+B, Left | Déplacement d’un caractère vers la gauche | +| Ctrl+F, Right | Déplacement d’un caractère vers la droite | +| Alt+B, Ctrl+Left | Déplacement d’un mot vers la gauche | +| Alt+F, Ctrl+Right | Déplacement d’un mot vers la droite | +| 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+D (si saisie commencée), Del | Suppression du caractère sous le curseur | +| Ctrl+D (si ligne vide) | Termine l’application (EOF) | +| Ctrl+H, Backspace | Suppression du caractère avant le curseur | +| Alt+D, Alt+Del | Suppression du prochain mot | +| Alt+Backspace | Suppression du précédent mot | +| Ctrl+C | Termine l’application (avec erreur) | +| Ctrl+P, Up (si ligne vide au départ) | Remonte à l’historique précédent | +| Ctrl+N, Down (si ligne vide au départ) | Descend dans l’historique | +| Ctrl+R, Up (si ligne non vide) | Recherche dans l’historique (par préfixe de ligne) à partir de la fin | +| Ctrl+S, Down (si ligne non vide) | Recherche dans l’historique (par préfixe de ligne) à partir du début | +| Ctrl+Y | Copie le dernier élément supprimé | +| Alt+Y | Remonte dans l’historique 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) | +| Tab, Up (si complétion commencée) | Complétion suivante | +| 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 | diff --git a/completer.go b/completer.go index eced51e..68724d4 100644 --- a/completer.go +++ b/completer.go @@ -11,20 +11,28 @@ import ( ) func coordToIndex(x, y, c, n int) int { + if x < 0 || x >= c || y < 0 { + return -1 + } + l, r := n/c, n%c switch { + case (x < r && y >= l+1) || (x >= r && y >= l): + return -1 case r == 0: return x*l + y case x < r: return x*(l+1) + y - case y >= l: - return -1 default: return r*(l+1) + (x-r)*l + y } } func indexToCoord(i, c, n int) (x, y int) { + if i < 0 || i >= n { + return -1, -1 + } + l, r := n/c, n%c switch { case r == 0: @@ -32,7 +40,7 @@ func indexToCoord(i, c, n int) (x, y int) { case i < r*(l+1): x, y = i/(l+1), i%(l+1) default: - j := i - r*l + j := i - r*(l+1) 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) formatChoices(currentWord string, terminalWidth, terminalHeight int) string { - var buf strings.Builder - l, cursor := c.choices.Len(), c.choices.Cursor() - if l == 0 { - return "" +func (c *completer) gridInfo(terminalWidth, terminalHeight int) (nbChoices, colWidth, nbCols, nbLines int, choices []string) { + nbChoices = c.choices.Len() + if nbChoices == 0 { + return } - var maxLen int - choices := make([]string, l) - for i := 0; i < l; i++ { + var wordWidth int + choices = make([]string, nbChoices) + for i := 0; i < nbChoices; i++ { 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) - lines := l / maxCol - if l%maxCol > 0 { - lines++ + nbCols = min(max(terminalWidth/wordWidth, 1), 5) + nbLines = nbChoices / nbCols + if nbChoices%nbCols > 0 { + nbLines++ } - lineMin, lineMax := 0, lines-1 - var isPartial bool - if isPartial = terminalHeight < lines; isPartial { - _, cursorLine := indexToCoord(cursor, maxCol, l) - if cursorLine < terminalHeight { - lineMax = min(terminalHeight, lines) - 1 + + colWidth = terminalWidth / nbCols + if colWidth > wordWidth { + colWidth = (colWidth + wordWidth) >> 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 { - lineMin, lineMax = cursorLine+1-min(terminalHeight, lines), cursorLine + x, y = indexToCoord(i, nbCols, nbChoices) + r = nbChoices % nbCols } } - colWith := terminalWidth / maxCol - if colWith > maxLen { - colWith = (colWith + maxLen) >> 1 + return +} + +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++ { if y > 0 { buf.WriteRune('\n') } - for x := 0; x < maxCol; x++ { - i := coordToIndex(x, y, maxCol, l) + for x := 0; x < nbCols; x++ { + i := coordToIndex(x, y, nbCols, nbChoices) if i >= 0 { word := choices[i] realSize, displaySize := len([]rune(word)), atom.VisibleWidth(word) - size := maxLen + realSize - displaySize + size := colWidth + realSize - displaySize word = strings.TrimPrefix(format.Left(word, size), currentWord) if i == cursor { 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 { - 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.WriteString(format.Apply(position, "bg_l_cyan", "black")) } diff --git a/fishline.go b/fishline.go index 76efeee..c435bf5 100644 --- a/fishline.go +++ b/fishline.go @@ -113,14 +113,45 @@ func (fl *Fishline) completionNext() (ok bool) { return } -func (fl *Fishline) fixState() bool { - ok1 := fl.st.RemoveSavedBuffer() - ok2 := fl.st.RemoveHistorySearch() - ok3 := fl.st.UnfocusHistory() - ok4 := fl.st.UnfocusYank() - ok5 := fl.removeCompletion() +func (fl *Fishline) getCompletionInfos(buffer ...string) (w, h int) { + var str string + if len(buffer) > 0 { + str = buffer[0] + } else { + 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) { @@ -286,7 +317,9 @@ func (fl *Fishline) execSequence(s atom.Sequence, isPassword bool) (fix, stop bo if !isPassword { l := fl.st.BufferLen() changeProposal = -1 - if l == 0 || fl.st.IsHistoryMode() { + if fl.isCompletionMode() { + noBeep = fl.completionPrev() + } else if l == 0 || fl.st.IsHistoryMode() { noBeep = fl.historyPrev() } else { noBeep = fl.searchHistoryPrev() @@ -296,20 +329,30 @@ func (fl *Fishline) execSequence(s atom.Sequence, isPassword bool) (fix, stop bo if !isPassword { l := fl.st.BufferLen() changeProposal = -1 - if l == 0 || fl.st.IsHistoryMode() { + if fl.isCompletionMode() { + noBeep = fl.completionNext() + } else if l == 0 || fl.st.IsHistoryMode() { noBeep = fl.historyNext() } else { noBeep = fl.searchHistoryNext() } } case atom.Right: // Move right - noBeep, fix = fl.st.Right(), true - if !noBeep && fl.proposal != "" { - noBeep, changeProposal = true, -1 - fl.st.SetBuffer(fl.proposal) + if fl.isCompletionMode() && !isPassword { + changeProposal, noBeep = -1, fl.completionRight() + } else { + noBeep, fix = fl.st.Right(), true + if !noBeep && fl.proposal != "" { + noBeep, changeProposal = true, -1 + fl.st.SetBuffer(fl.proposal) + } } 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 fl.st.ToggleReplace() 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")) } if c, ok := fl.completion.Get(); ok { - w, h := fl.st.Width(), fl.st.Height()-1 - wb := atom.VisibleWidth(fl.st.GetPrompt() + buffer + suffix) - hb := wb / w - if wb%w > 0 { - hb++ - } - choices := c.formatChoices(fl.currentWord, w, h-hb) + w, h := fl.getCompletionInfos(buffer + suffix) + choices := c.formatChoices(fl.currentWord, w, h) if choices != "" { output.WriteString("\n") - output.WriteString(c.formatChoices(fl.currentWord, w, h-hb)) + output.WriteString(choices) } } } diff --git a/go.mod b/go.mod index 0d9fd16..e2cb70a 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module gitea.zaclys.com/bvaudour/fishline +module gitea.zaclys.com/bvaudour/gfishline go 1.22.0