Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions inline_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package readline

import "testing"

func TestInlineSuggestionAPI(t *testing.T) {
rl := NewShell()

rl.SetInlineSuggestion("status --verbose")
if got := rl.GetInlineSuggestion(); got != "status --verbose" {
t.Fatalf("inline suggestion = %q, want status --verbose", got)
}

rl.ClearInlineSuggestion()
if got := rl.GetInlineSuggestion(); got != "" {
t.Fatalf("inline suggestion after clear = %q, want empty", got)
}
}
55 changes: 50 additions & 5 deletions internal/display/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package display

import (
"regexp"
"strings"

"github.com/reeflective/readline/inputrc"
"github.com/reeflective/readline/internal/color"
Expand Down Expand Up @@ -43,6 +44,7 @@ type Engine struct {
line *core.Line
suggested core.Line
cursor *core.Cursor
inline string
selection *core.Selection
histories *history.Sources
prompt *ui.Prompt
Expand Down Expand Up @@ -71,6 +73,46 @@ func Init(e *Engine, highlighter func([]rune) string) {
e.highlighter = highlighter
}

// SetInlineSuggestion sets a suggestion to display after the cursor.
func (e *Engine) SetInlineSuggestion(suggestion string) {
e.inline = suggestion
}

// ClearInlineSuggestion clears the current inline suggestion.
func (e *Engine) ClearInlineSuggestion() {
e.inline = ""
}

// GetInlineSuggestion returns the current inline suggestion.
func (e *Engine) GetInlineSuggestion() string {
return e.inline
}

func (e *Engine) inlineSuggestionApplies(currentLine string) bool {
if e.inline == "" {
return false
}
if e.cursor.Pos() != e.line.Len() {
return false
}

return strings.HasPrefix(e.inline, currentLine) && len(e.inline) > len(currentLine)
}

func (e *Engine) coordinatesLine(suggested bool) *core.Line {
currentLine := string(*e.line)
if e.opts.GetBool("history-autosuggest") && suggested && e.suggested.Len() > e.line.Len() {
return &e.suggested
}
if e.inlineSuggestionApplies(currentLine) {
inline := core.Line{}
inline.Set([]rune(e.inline)...)
return &inline
}

return e.line
}

// PrintPrimaryPrompt redraws the primary prompt.
// There are relatively few cases where you want to use this.
// It is currently only used when using clear-screen commands.
Expand Down Expand Up @@ -202,11 +244,7 @@ func (e *Engine) computeCoordinates(suggested bool) {
e.cursorCol, e.cursorRow = core.CoordinatesCursor(e.cursor, e.startCols)

// Get the number of rows used by the line, and the end line X pos.
if e.opts.GetBool("history-autosuggest") && suggested {
e.lineCol, e.lineRows = core.CoordinatesLine(&e.suggested, e.startCols)
} else {
e.lineCol, e.lineRows = core.CoordinatesLine(e.line, e.startCols)
}
e.lineCol, e.lineRows = core.CoordinatesLine(e.coordinatesLine(suggested), e.startCols)

e.primaryPrinted = false
}
Expand All @@ -231,8 +269,15 @@ func (e *Engine) displayLine() {
line = e.highlightLine([]rune(line), *e.selection)

// Get the subset of the suggested line to print.
suggestionAdded := false
if len(e.suggested) > e.line.Len() && e.opts.GetBool("history-autosuggest") {
line += color.Dim + color.Fmt(color.Fg+"242") + string(e.suggested[e.line.Len():]) + color.Reset
suggestionAdded = true
}

currentLine := string(*e.line)
if !suggestionAdded && e.inlineSuggestionApplies(currentLine) {
line += color.Dim + color.Fmt(color.Fg+"242") + e.inline[len(currentLine):] + color.Reset
}

// Format tabs as spaces, for consistent display
Expand Down
73 changes: 73 additions & 0 deletions internal/display/inline_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package display

import (
"testing"

"github.com/reeflective/readline/inputrc"
"github.com/reeflective/readline/internal/core"
)

func TestInlineSuggestionAppliesOnlyAtLineEndWithPrefix(t *testing.T) {
line := core.Line{}
line.Set([]rune("sta")...)
cursor := core.NewCursor(&line)
cursor.Set(line.Len())

engine := &Engine{line: &line, cursor: cursor}
engine.SetInlineSuggestion("status")

if !engine.inlineSuggestionApplies("sta") {
t.Fatal("inline suggestion should apply when it extends the line at the cursor")
}

cursor.Set(1)
if engine.inlineSuggestionApplies("sta") {
t.Fatal("inline suggestion should not apply when the cursor is not at the end")
}

cursor.Set(line.Len())
engine.SetInlineSuggestion("stop")
if engine.inlineSuggestionApplies("sta") {
t.Fatal("inline suggestion should not apply when it does not extend the current line")
}
}

func TestCoordinatesLineUsesInlineSuggestion(t *testing.T) {
line := core.Line{}
line.Set([]rune("sta")...)
cursor := core.NewCursor(&line)
cursor.Set(line.Len())

engine := &Engine{
line: &line,
cursor: cursor,
opts: inputrc.NewConfig(),
}
engine.SetInlineSuggestion("status --verbose")

if got := engine.coordinatesLine(true); string(*got) != "status --verbose" {
t.Fatalf("coordinates line = %q, want inline suggestion", string(*got))
}
}

func TestCoordinatesLinePrefersHistorySuggestion(t *testing.T) {
line := core.Line{}
line.Set([]rune("sta")...)
cursor := core.NewCursor(&line)
cursor.Set(line.Len())

cfg := inputrc.NewConfig()
_ = cfg.Set("history-autosuggest", true)

engine := &Engine{
line: &line,
cursor: cursor,
opts: cfg,
}
engine.suggested.Set([]rune("status-from-history")...)
engine.SetInlineSuggestion("status-from-inline")

if got := engine.coordinatesLine(true); string(*got) != "status-from-history" {
t.Fatalf("coordinates line = %q, want history suggestion", string(*got))
}
}
9 changes: 8 additions & 1 deletion internal/display/refresh.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func (e *Engine) Refresh() {
// Recompute coordinates with the new indentation/cursor position.
if e.line.Lines() > 0 {
e.cursorCol, e.cursorRow = core.CoordinatesCursor(e.cursor, e.startCols)
e.lineCol, e.lineRows = core.CoordinatesLine(e.line, e.startCols)
e.lineCol, e.lineRows = core.CoordinatesLine(e.coordinatesLine(true), e.startCols)
}

// Ensure that we have enough space to print the line.
Expand Down Expand Up @@ -268,8 +268,15 @@ func (e *Engine) displayLineRefactored() {
// Apply visual selections highlighting if any
line = e.highlightLine([]rune(line), *e.selection)
// Get the subset of the suggested line to print.
suggestionAdded := false
if len(e.suggested) > e.line.Len() && e.opts.GetBool("history-autosuggest") {
line += color.Dim + color.Fmt(color.Fg+"242") + string(e.suggested[e.line.Len():]) + color.Reset
suggestionAdded = true
}

currentLine := string(*e.line)
if !suggestionAdded && e.inlineSuggestionApplies(currentLine) {
line += color.Dim + color.Fmt(color.Fg+"242") + e.inline[len(currentLine):] + color.Reset
}
// Format tabs as spaces, for consistent display
line = strutil.FormatTabs(line) + term.ClearLineAfter
Expand Down
27 changes: 27 additions & 0 deletions shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,30 @@ func (rl *Shell) PrintTransientf(msg string, args ...any) (n int, err error) {

return
}

// SetInlineSuggestion sets a suggestion to display after the cursor.
func (rl *Shell) SetInlineSuggestion(suggestion string) {
if rl == nil || rl.Display == nil {
return
}

rl.Display.SetInlineSuggestion(suggestion)
}

// ClearInlineSuggestion clears the current inline suggestion.
func (rl *Shell) ClearInlineSuggestion() {
if rl == nil || rl.Display == nil {
return
}

rl.Display.ClearInlineSuggestion()
}

// GetInlineSuggestion returns the current inline suggestion.
func (rl *Shell) GetInlineSuggestion() string {
if rl == nil || rl.Display == nil {
return ""
}

return rl.Display.GetInlineSuggestion()
}
Loading