From 6ea25a22b3a1d59b2fd640894ede364523edd689 Mon Sep 17 00:00:00 2001 From: TorchedSammy <38820196+TorchedSammy@users.noreply.github.com> Date: Sat, 26 Mar 2022 17:34:09 -0400 Subject: [PATCH] feat: add inline hint text and change what were hints previously to info (closes #126) --- api.go | 14 +++++++++++ readline/events.go | 2 +- readline/hint.go | 10 ++++---- readline/history.go | 4 ++-- readline/info.go | 56 +++++++++++++++++++++++++++++++++++++++++++ readline/instance.go | 33 ++++++++++++++++++------- readline/line.go | 4 ++-- readline/prompt.go | 10 ++++---- readline/readline.go | 18 +++++++------- readline/register.go | 6 ++--- readline/tab.go | 36 ++++++++++++++-------------- readline/tabfind.go | 2 +- readline/update.go | 52 ++++++++++++++++++++++++++-------------- readline/vim.go | 16 ++++++------- readline/vimdelete.go | 2 +- rl.go | 24 +++++++++++++++++-- 16 files changed, 207 insertions(+), 82 deletions(-) create mode 100644 readline/info.go diff --git a/api.go b/api.go index 93a8c6a..9cfe3c9 100644 --- a/api.go +++ b/api.go @@ -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 +} diff --git a/readline/events.go b/readline/events.go index a89e5b7..5d63076 100644 --- a/readline/events.go +++ b/readline/events.go @@ -7,7 +7,7 @@ type EventReturn struct { ForwardKey bool ClearHelpers bool CloseReadline bool - HintText []rune + InfoText []rune NewLine []rune NewPos int } diff --git a/readline/hint.go b/readline/hint.go index 37d99ac..6c6a67c 100644 --- a/readline/hint.go +++ b/readline/hint.go @@ -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{} } diff --git a/readline/history.go b/readline/history.go index 8aff2a8..41200c6 100644 --- a/readline/history.go +++ b/readline/history.go @@ -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) diff --git a/readline/info.go b/readline/info.go new file mode 100644 index 0000000..269157d --- /dev/null +++ b/readline/info.go @@ -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{} +} diff --git a/readline/instance.go b/readline/instance.go index 48e4398..2530071 100644 --- a/readline/instance.go +++ b/readline/instance.go @@ -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() diff --git a/readline/line.go b/readline/line.go index 580ac3d..2030143 100644 --- a/readline/line.go +++ b/readline/line.go @@ -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)) } } diff --git a/readline/prompt.go b/readline/prompt.go index 2f3b80a..a19b56c 100644 --- a/readline/prompt.go +++ b/readline/prompt.go @@ -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 diff --git a/readline/readline.go b/readline/readline.go index 056f10c..70724d4 100644 --- a/readline/readline.go +++ b/readline/readline.go @@ -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 diff --git a/readline/register.go b/readline/register.go index 5cc03af..4372b89 100644 --- a/readline/register.go +++ b/readline/register.go @@ -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{ diff --git a/readline/tab.go b/readline/tab.go index 231f3f8..c3f9c37 100644 --- a/readline/tab.go +++ b/readline/tab.go @@ -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 diff --git a/readline/tabfind.go b/readline/tabfind.go index 44487bf..5abe307 100644 --- a/readline/tabfind.go +++ b/readline/tabfind.go @@ -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 diff --git a/readline/update.go b/readline/update.go index 2f58cdc..979dbc9 100644 --- a/readline/update.go +++ b/readline/update.go @@ -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 { diff --git a/readline/vim.go b/readline/vim.go index f421d84..886927b 100644 --- a/readline/vim.go +++ b/readline/vim.go @@ -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() diff --git a/readline/vimdelete.go b/readline/vimdelete.go index b836019..7a07259 100644 --- a/readline/vimdelete.go +++ b/readline/vimdelete.go @@ -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() diff --git a/rl.go b/rl.go index 659e740..ca4edab 100644 --- a/rl.go +++ b/rl.go @@ -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 {