feat: add inline hint text and change what were hints previously to info (closes #126)

pull/128/head
TorchedSammy 2022-03-26 17:34:09 -04:00
parent 577f00dfef
commit 6ea25a22b3
Signed by: sammyette
GPG Key ID: 904FC49417B44DCD
16 changed files with 207 additions and 82 deletions

14
api.go
View File

@ -28,6 +28,7 @@ var exports = map[string]lua.LGFunction {
"exec": hlexec,
"runnerMode": hlrunnerMode,
"goro": hlgoro,
"hinter": hlhinter,
"multiprompt": hlmlprompt,
"prependPath": hlprependPath,
"prompt": hlprompt,
@ -503,3 +504,16 @@ func hlrunnerMode(L *lua.LState) int {
return 0
}
// hinter(cb)
// Sets the hinter function. This will be called on every key insert to determine
// what text to use as an inline hint. The callback is passed 2 arguments:
// the current line and the position. It is expected to return a string
// which will be used for the hint.
// --- @param cb function
func hlhinter(L *lua.LState) int {
hinterCb := L.CheckFunction(1)
hinter = hinterCb
return 0
}

View File

@ -7,7 +7,7 @@ type EventReturn struct {
ForwardKey bool
ClearHelpers bool
CloseReadline bool
HintText []rune
InfoText []rune
NewLine []rune
NewPos int
}

View File

@ -4,10 +4,12 @@ import "regexp"
// SetHintText - a nasty function to force writing a new hint text. It does not update helpers, it just renders
// them, so the hint will survive until the helpers (thus including the hint) will be updated/recomputed.
/*
func (rl *Instance) SetHintText(s string) {
rl.hintText = []rune(s)
rl.renderHelpers()
}
*/
func (rl *Instance) getHintText() {
@ -27,7 +29,7 @@ func (rl *Instance) getHintText() {
// writeHintText - only writes the hint text and computes its offsets.
func (rl *Instance) writeHintText() {
if len(rl.hintText) == 0 {
rl.hintY = 0
//rl.hintY = 0
return
}
@ -41,16 +43,16 @@ func (rl *Instance) writeHintText() {
wrapped, hintLen := WrapText(string(rl.hintText), width)
offset += hintLen
rl.hintY = offset
// rl.hintY = offset
hintText := string(wrapped)
if len(hintText) > 0 {
print("\r" + rl.HintFormatting + string(hintText) + seqReset)
print(rl.HintFormatting + string(hintText) + seqReset)
}
}
func (rl *Instance) resetHintText() {
rl.hintY = 0
//rl.hintY = 0
rl.hintText = []rune{}
}

View File

@ -183,13 +183,13 @@ func (rl *Instance) completeHistory() (hist []*CompletionGroup) {
return
}
history = rl.altHistory
rl.histHint = []rune(rl.altHistName + ": ")
rl.histInfo = []rune(rl.altHistName + ": ")
} else {
if rl.mainHistory == nil {
return
}
history = rl.mainHistory
rl.histHint = []rune(rl.mainHistName + ": ")
rl.histInfo = []rune(rl.mainHistName + ": ")
}
hist[0].init(rl)

56
readline/info.go 100644
View File

@ -0,0 +1,56 @@
package readline
import "regexp"
// SetInfoText - a nasty function to force writing a new info text. It does not update helpers, it just renders
// them, so the info will survive until the helpers (thus including the info) will be updated/recomputed.
func (rl *Instance) SetInfoText(s string) {
rl.infoText = []rune(s)
rl.renderHelpers()
}
func (rl *Instance) getInfoText() {
if !rl.modeAutoFind && !rl.modeTabFind {
// Return if no infos provided by the user/engine
if rl.InfoText == nil {
rl.resetInfoText()
return
}
// The info text also works with the virtual completion line system.
// This way, the info is also refreshed depending on what we are pointing
// at with our cursor.
rl.infoText = rl.InfoText(rl.getCompletionLine())
}
}
// writeInfoText - only writes the info text and computes its offsets.
func (rl *Instance) writeInfoText() {
if len(rl.infoText) == 0 {
rl.infoY = 0
return
}
width := GetTermWidth()
// Wraps the line, and counts the number of newlines in the string,
// adjusting the offset as well.
re := regexp.MustCompile(`\r?\n`)
newlines := re.Split(string(rl.infoText), -1)
offset := len(newlines)
wrapped, infoLen := WrapText(string(rl.infoText), width)
offset += infoLen
rl.infoY = offset
infoText := string(wrapped)
if len(infoText) > 0 {
print("\r" + rl.InfoFormatting + string(infoText) + seqReset)
}
}
func (rl *Instance) resetInfoText() {
rl.infoY = 0
rl.infoText = []rune{}
}

View File

@ -110,7 +110,7 @@ type Instance struct {
searchMode FindMode // Used for varying hints, and underlying functions called
regexSearch *regexp.Regexp // Holds the current search regex match
mainHist bool // Which history stdin do we want
histHint []rune // We store a hist hint, for dual history sources
histInfo []rune // We store a piece of hist info, for dual history sources
//
// History -----------------------------------------------------------------------------------
@ -134,19 +134,33 @@ type Instance struct {
histNavIdx int // Used for quick history navigation.
//
// Hints -------------------------------------------------------------------------------------
// Info -------------------------------------------------------------------------------------
// HintText is a helper function which displays hint text the prompt.
// HintText takes the line input from the promt and the cursor position.
// InfoText is a helper function which displays infio text below the prompt.
// InfoText takes the line input from the prompt and the cursor position.
// It returns the info text to display.
InfoText func([]rune, int) []rune
// InfoColor is any ANSI escape codes you wish to use for info formatting. By
// default this will just be blue.
InfoFormatting string
infoText []rune // The actual info text
infoY int // Offset to info, if it spans multiple lines
//
// Hints -----------------------------------------------------------------------------------
// HintText is a helper function which displays hint text right after the user's input.
// It takes the line input and cursor position.
// It returns the hint text to display.
HintText func([]rune, int) []rune
// HintColor any ANSI escape codes you wish to use for hint formatting. By
// default this will just be blue.
// HintFormatting is just a string to use as the formatting for the hint. By default
// this will be a grey color.
HintFormatting string
hintText []rune // The actual hint text
hintY int // Offset to hints, if it spans multiple lines
hintText []rune
//
// Vim Operatng Parameters -------------------------------------------------------------------
@ -205,7 +219,8 @@ func NewInstance() *Instance {
rl.HistoryAutoWrite = true
// Others
rl.HintFormatting = seqFgBlue
rl.InfoFormatting = seqFgBlue
rl.HintFormatting = "\x1b[2m"
rl.evtKeyPress = make(map[string]func(string, []rune, int) *EventReturn)
rl.TempDirectory = os.TempDir()

View File

@ -57,9 +57,9 @@ func (rl *Instance) echo() {
// Print the input line with optional syntax highlighting
if rl.SyntaxHighlighter != nil {
print(rl.SyntaxHighlighter(line) + " ")
print(rl.SyntaxHighlighter(line))
} else {
print(string(line) + " ")
print(string(line))
}
}

View File

@ -20,7 +20,7 @@ func (rl *Instance) RefreshPromptLog(log string) (err error) {
// We adjust cursor movement, depending on which mode we're currently in.
if !rl.modeTabCompletion {
rl.tcUsedY = 1
// Account for the hint line
// Account for the info line
} else if rl.modeTabCompletion && rl.modeAutoFind {
rl.tcUsedY = 0
} else {
@ -40,7 +40,7 @@ func (rl *Instance) RefreshPromptLog(log string) (err error) {
moveCursorUp(1)
}
rl.stillOnRefresh = true
moveCursorUp(rl.hintY + rl.tcUsedY)
moveCursorUp(rl.infoY + rl.tcUsedY)
moveCursorBackwards(GetTermWidth())
print("\r\n" + seqClearScreenBelow)
@ -73,7 +73,7 @@ func (rl *Instance) RefreshPromptInPlace(prompt string) (err error) {
// Prompt data intependent
if !rl.modeTabCompletion {
rl.tcUsedY = 1
// Account for the hint line
// Account for the info line
} else if rl.modeTabCompletion && rl.modeAutoFind {
rl.tcUsedY = 0
} else {
@ -91,7 +91,7 @@ func (rl *Instance) RefreshPromptInPlace(prompt string) (err error) {
// Clear the input line and everything below
print(seqClearLine)
moveCursorUp(rl.hintY + rl.tcUsedY)
moveCursorUp(rl.infoY + rl.tcUsedY)
moveCursorBackwards(GetTermWidth())
print("\r\n" + seqClearScreenBelow)
@ -118,7 +118,7 @@ func (rl *Instance) RefreshPromptCustom(prompt string, offset int, clearLine boo
// We adjust cursor movement, depending on which mode we're currently in.
if !rl.modeTabCompletion {
rl.tcUsedY = 1
} else if rl.modeTabCompletion && rl.modeAutoFind { // Account for the hint line
} else if rl.modeTabCompletion && rl.modeAutoFind { // Account for the info line
rl.tcUsedY = 0
} else {
rl.tcUsedY = 1

View File

@ -40,10 +40,10 @@ func (rl *Instance) Readline() (string, error) {
rl.posY = 0
rl.tcPrefix = ""
// Completion && hints init
rl.resetHintText()
// Completion && infos init
rl.resetInfoText()
rl.resetTabCompletion()
rl.getHintText()
rl.getInfoText()
// History Init
// We need this set to the last command, so that we can access it quickly
@ -63,7 +63,7 @@ func (rl *Instance) Readline() (string, error) {
return string(rl.line), nil
}
// Finally, print any hints or completions
// Finally, print any info or completions
// if the TabCompletion engines so desires
rl.renderHelpers()
@ -128,8 +128,8 @@ func (rl *Instance) Readline() (string, error) {
rl.updateHelpers()
}
if len(ret.HintText) > 0 {
rl.hintText = ret.HintText
if len(ret.InfoText) > 0 {
rl.infoText = ret.InfoText
rl.clearHelpers()
rl.renderHelpers()
}
@ -174,8 +174,8 @@ func (rl *Instance) Readline() (string, error) {
}
print(seqClearScreenBelow)
rl.resetHintText()
rl.getHintText()
rl.resetInfoText()
rl.getInfoText()
rl.renderHelpers()
// Line Editing ------------------------------------------------------------------------------------
@ -533,6 +533,7 @@ func (rl *Instance) editorInput(r []rune) {
// We don't need it when inserting text.
rl.histNavIdx = 0
rl.insert(r)
rl.writeHintText()
}
if len(rl.multisplit) == 0 {
@ -696,6 +697,7 @@ func (rl *Instance) escapeSeq(r []rune) {
rl.updateHelpers()
return
case seqCtrlRightArrow:
rl.insert(rl.hintText)
rl.moveCursorByAdjust(rl.viJumpW(tokeniseLine))
rl.updateHelpers()
return

View File

@ -259,9 +259,9 @@ func (r *registers) resetRegister() {
// The user can show registers completions and insert, no matter the cursor position.
func (rl *Instance) completeRegisters() (groups []*CompletionGroup) {
// We set the hint exceptionally
hint := BLUE + "-- registers --" + RESET
rl.hintText = []rune(hint)
// We set the info exceptionally
info := BLUE + "-- registers --" + RESET
rl.infoText = []rune(info)
// Make the groups
anonRegs := &CompletionGroup{

View File

@ -93,16 +93,16 @@ func (rl *Instance) getTabSearchCompletion() {
}
rl.getCurrentGroup()
// Set the hint for this completion mode
rl.hintText = append([]rune("Completion search: "), rl.tfLine...)
// Set the info for this completion mode
rl.infoText = append([]rune("Completion search: "), rl.tfLine...)
for _, g := range rl.tcGroups {
g.updateTabFind(rl)
}
// If total number of matches is zero, we directly change the hint, and return
// If total number of matches is zero, we directly change the info, and return
if comps, _, _ := rl.getCompletionCount(); comps == 0 {
rl.hintText = append(rl.hintText, []rune(DIM+RED+" ! no matches (Ctrl-G/Esc to cancel)"+RESET)...)
rl.infoText = append(rl.infoText, []rune(DIM+RED+" ! no matches (Ctrl-G/Esc to cancel)"+RESET)...)
}
}
@ -117,25 +117,25 @@ func (rl *Instance) getHistorySearchCompletion() {
rl.tcGroups = checkNilItems(rl.tcGroups) // Avoid nil maps in groups
rl.getCurrentGroup() // Make sure there is a current group
// The history hint is already set, but overwrite it if we don't have completions
// The history info is already set, but overwrite it if we don't have completions
if len(rl.tcGroups[0].Suggestions) == 0 {
rl.histHint = []rune(fmt.Sprintf("%s%s%s %s", DIM, RED,
rl.histInfo = []rune(fmt.Sprintf("%s%s%s %s", DIM, RED,
"No command history source, or empty (Ctrl-G/Esc to cancel)", RESET))
rl.hintText = rl.histHint
rl.infoText = rl.histInfo
return
}
// Set the hint line with everything
rl.histHint = append([]rune("\033[38;5;183m"+string(rl.histHint)+RESET), rl.tfLine...)
rl.histHint = append(rl.histHint, []rune(RESET)...)
rl.hintText = rl.histHint
// Set the info line with everything
rl.histInfo = append([]rune("\033[38;5;183m"+string(rl.histInfo)+RESET), rl.tfLine...)
rl.histInfo = append(rl.histInfo, []rune(RESET)...)
rl.infoText = rl.histInfo
// Refresh filtered candidates
rl.tcGroups[0].updateTabFind(rl)
// If no items matched history, add hint text that we failed to search
// If no items matched history, add info text that we failed to search
if len(rl.tcGroups[0].Suggestions) == 0 {
rl.hintText = append(rl.histHint, []rune(DIM+RED+" ! no matches (Ctrl-G/Esc to cancel)"+RESET)...)
rl.infoText = append(rl.histInfo, []rune(DIM+RED+" ! no matches (Ctrl-G/Esc to cancel)"+RESET)...)
return
}
}
@ -298,15 +298,15 @@ func (rl *Instance) cropCompletions(comps string) (cropped string, usedY int) {
// Else we go on, but we have more comps than what allowed:
// we will add a line to the end of the comps, giving the actualized
// number of completions remaining and not printed
var moreComps = func(cropped string, offset int) (hinted string, noHint bool) {
var moreComps = func(cropped string, offset int) (infoed string, noInfo bool) {
_, _, adjusted := rl.getCompletionCount()
remain := adjusted - offset
if remain == 0 {
return cropped, true
}
hint := fmt.Sprintf(DIM+YELLOW+" %d more completions... (scroll down to show)"+RESET+"\n", remain)
hinted = cropped + hint
return hinted, false
info := fmt.Sprintf(DIM+YELLOW+" %d more completions... (scroll down to show)"+RESET+"\n", remain)
infoed = cropped + info
return infoed, false
}
// Get the current absolute candidate position (prev groups x suggestions + curGroup.tcPosY)
@ -509,7 +509,7 @@ func (rl *Instance) hasOneCandidate() bool {
// - The terminal lengh
// we use this function to prompt for confirmation before printing comps.
func (rl *Instance) promptCompletionConfirm(sentence string) {
rl.hintText = []rune(sentence)
rl.infoText = []rune(sentence)
rl.compConfirmWait = true
rl.viUndoSkipAppend = true

View File

@ -33,7 +33,7 @@ func (rl *Instance) updateTabFind(r []rune) {
var err error
rl.regexSearch, err = regexp.Compile("(?i)" + string(rl.tfLine))
if err != nil {
rl.hintText = []rune(Red("Failed to match search regexp"))
rl.infoText = []rune(Red("Failed to match search regexp"))
}
// We update and print

View File

@ -1,12 +1,13 @@
package readline
// updateHelpers is a key part of the whole refresh process:
// it should coordinate reprinting the input line, any hints and completions
// it should coordinate reprinting the input line, any Infos and completions
// and manage to get back to the current (computed) cursor coordinates
func (rl *Instance) updateHelpers() {
// Load all hints & completions before anything.
// Thus overwrites anything having been dirtily added/forced/modified, like rl.SetHintText()
// Load all Infos & completions before anything.
// Thus overwrites anything having been dirtily added/forced/modified, like rl.SetInfoText()
rl.getInfoText()
rl.getHintText()
if rl.modeTabCompletion {
rl.getTabCompletion()
@ -75,11 +76,11 @@ func (rl *Instance) resetHelpers() {
rl.modeAutoFind = false
// Now reset all below-input helpers
rl.resetHintText()
rl.resetInfoText()
rl.resetTabCompletion()
}
// clearHelpers - Clears everything: prompt, input, hints & comps,
// clearHelpers - Clears everything: prompt, input, Infos & comps,
// and comes back at the prompt.
func (rl *Instance) clearHelpers() {
@ -97,25 +98,40 @@ func (rl *Instance) clearHelpers() {
moveCursorForwards(rl.posX)
}
// renderHelpers - pritns all components (prompt, line, hints & comps)
// renderHelpers - pritns all components (prompt, line, Infos & comps)
// and replaces the cursor to its current position. This function never
// computes or refreshes any value, except from inside the echo function.
func (rl *Instance) renderHelpers() {
// Optional, because neutral on placement
// when the instance is in this state we want it to be "below" the user's
// input for it to be aligned properly
if !rl.compConfirmWait {
rl.writeHintText()
}
rl.echo()
if rl.modeTabCompletion {
// in tab complete mode we want it to update
// when something has been selected
// (dynamic!!)
rl.getHintText()
rl.writeHintText()
} else if !rl.compConfirmWait {
// for the same reason above, do nothing here
} else {
rl.writeHintText()
}
// Go at beginning of first line after input remainder
moveCursorDown(rl.fullY - rl.posY)
moveCursorBackwards(GetTermWidth())
// Print hints, check for any confirmation hint current.
// (do not overwrite the confirmation question hint)
// Print Infos, check for any confirmation Info current.
// (do not overwrite the confirmation question Info)
if !rl.compConfirmWait {
if len(rl.hintText) > 0 {
if len(rl.infoText) > 0 {
print("\n")
}
rl.writeHintText()
rl.writeInfoText()
moveCursorBackwards(GetTermWidth())
// Print completions and go back to beginning of this line
@ -126,17 +142,17 @@ func (rl *Instance) renderHelpers() {
}
// If we are still waiting for the user to confirm too long completions
// Immediately refresh the hints
// Immediately refresh the Infos
if rl.compConfirmWait {
print("\n")
rl.writeHintText()
rl.getHintText()
rl.writeInfoText()
rl.getInfoText()
moveCursorBackwards(GetTermWidth())
}
// Anyway, compensate for hint printout
if len(rl.hintText) > 0 {
moveCursorUp(rl.hintY)
// Anyway, compensate for Info printout
if len(rl.infoText) > 0 {
moveCursorUp(rl.infoY)
} else if !rl.compConfirmWait {
moveCursorUp(1)
} else if rl.compConfirmWait {

View File

@ -399,22 +399,22 @@ func (rl *Instance) refreshVimStatus() {
rl.updateHelpers()
}
// viHintMessage - lmorg's way of showing Vim status is to overwrite the hint.
// viInfoMessage - lmorg's way of showing Vim status is to overwrite the info.
// Currently not used, as there is a possibility to show the current Vim mode in the prompt.
func (rl *Instance) viHintMessage() {
func (rl *Instance) viInfoMessage() {
switch rl.modeViMode {
case VimKeys:
rl.hintText = []rune("-- VIM KEYS -- (press `i` to return to normal editing mode)")
rl.infoText = []rune("-- VIM KEYS -- (press `i` to return to normal editing mode)")
case VimInsert:
rl.hintText = []rune("-- INSERT --")
rl.infoText = []rune("-- INSERT --")
case VimReplaceOnce:
rl.hintText = []rune("-- REPLACE CHARACTER --")
rl.infoText = []rune("-- REPLACE CHARACTER --")
case VimReplaceMany:
rl.hintText = []rune("-- REPLACE --")
rl.infoText = []rune("-- REPLACE --")
case VimDelete:
rl.hintText = []rune("-- DELETE --")
rl.infoText = []rune("-- DELETE --")
default:
rl.getHintText()
rl.getInfoText()
}
rl.clearHelpers()

View File

@ -33,7 +33,7 @@ func (rl *Instance) viDelete(r rune) {
rl.saveBufToRegister(rl.line)
rl.clearLine()
rl.resetHelpers()
rl.getHintText()
rl.getInfoText()
case 'e':
vii := rl.getViIterations()

24
rl.go
View File

@ -13,6 +13,7 @@ type lineReader struct {
rl *readline.Instance
}
var fileHist *fileHistory
var hinter lua.LValue
// other gophers might hate this naming but this is local, shut up
func newLineReader(prompt string, noHist bool) *lineReader {
@ -44,6 +45,25 @@ func newLineReader(prompt string, noHist bool) *lineReader {
}
hooks.Em.Emit("hilbish.vimAction", actionStr, args)
}
rl.HintText = func(line []rune, pos int) []rune {
err := l.CallByParam(lua.P{
Fn: hinter,
NRet: 1,
Protect: true,
}, lua.LString(string(line)), lua.LNumber(pos))
if err != nil {
fmt.Println(err)
return []rune{}
}
retVal := l.Get(-1)
hintText := ""
if luaStr, ok := retVal.(lua.LString); retVal != lua.LNil && ok {
hintText = luaStr.String()
}
return []rune(hintText)
}
rl.TabCompleter = func(line []rune, pos int, _ readline.DelayedTabContext) (string, []*readline.CompletionGroup) {
ctx := string(line)
var completions []string
@ -253,7 +273,7 @@ func (lr *lineReader) luaGetHistory(L *lua.LState) int {
cmd, _ := fileHist.GetLine(idx)
L.Push(lua.LString(cmd))
return 0
return 1
}
func (lr *lineReader) luaAllHistory(L *lua.LState) int {
@ -267,7 +287,7 @@ func (lr *lineReader) luaAllHistory(L *lua.LState) int {
L.Push(tbl)
return 0
return 1
}
func (lr *lineReader) luaClearHistory(l *lua.LState) int {