Compare commits

...

7 Commits

Author SHA1 Message Date
TorchedSammy bd4e0df7b3
feat: select 1st item on history find menu (closes #148)
cancels on escape unless the user moves to another item
2022-11-25 20:14:29 -04:00
TorchedSammy ebec585690
fix: binary completion with spaces (closes #210) 2022-11-25 19:35:26 -04:00
TorchedSammy ff4609e432
feat: add hilbish.cancel hook (closes #213) 2022-11-25 19:21:36 -04:00
TorchedSammy ef3e7d92bc
chore: update changelog 2022-11-25 19:13:32 -04:00
TorchedSammy d6338fc021
fix(readline): make completion search menu display 2022-11-25 19:08:38 -04:00
TorchedSammy 3eaeb6a5da
fix(readline): grip completion menu fixes
- dont print item left justified if the max number
of cells is 1 (this fixes issues in cjk as an example)
- trim items that are longer than the terminal width
2022-11-25 18:39:18 -04:00
TorchedSammy 8b547f2af0
feat: make tab completion work with spaces and escaped characters 2022-11-25 16:56:35 -04:00
6 changed files with 83 additions and 32 deletions

View File

@ -76,6 +76,7 @@ disables commands being added to history.
random errors introduced with the new Lua runtime (see [#197]) random errors introduced with the new Lua runtime (see [#197])
- `bait.release(name, catcher)` removes `handler` for the named `event` - `bait.release(name, catcher)` removes `handler` for the named `event`
- `exec`, `clear` and `cat` builtin commands - `exec`, `clear` and `cat` builtin commands
- `hilbish.cancel` hook
[#197]: https://github.com/Rosettea/Hilbish/issues/197 [#197]: https://github.com/Rosettea/Hilbish/issues/197
@ -151,6 +152,12 @@ an error of missing format variable
- Fix an error with sh syntax in aliases - Fix an error with sh syntax in aliases
- Prompt now works with east asian characters (CJK) - Prompt now works with east asian characters (CJK)
- Set back the prompt to normal after exiting the continue prompt with ctrl-d - Set back the prompt to normal after exiting the continue prompt with ctrl-d
- Users can now tab complete files with spaces while quoted or with escaped spaces.
This means a query of `Files\ to\ ` with file names of `Files to tab complete` and `Files to complete`
will result in the files being completed.
- Fixed grid menu display if cell width ends up being the width of the terminal
- Cut off item names in grid menu if its longer than cell width
- Fix completion search menu disappearing
## [2.0.0-rc1] - 2022-09-14 ## [2.0.0-rc1] - 2022-09-14
This is a pre-release version of Hilbish for testing. To see the changelog, This is a pre-release version of Hilbish for testing. To see the changelog,

View File

@ -11,15 +11,49 @@ import (
rt "github.com/arnodel/golua/runtime" rt "github.com/arnodel/golua/runtime"
) )
func splitQuote(str string) []string { var charEscapeMap = []string{
"\"", "\\\"",
"'", "\\'",
"`", "\\`",
" ", "\\ ",
"(", "\\(",
")", "\\)",
"[", "\\[",
"]", "\\]",
"$", "\\$",
"&", "\\&",
"*", "\\*",
">", "\\>",
"<", "\\<",
"|", "\\|",
}
var charEscapeMapInvert = invert(charEscapeMap)
var escapeReplaer = strings.NewReplacer(charEscapeMap...)
var escapeInvertReplaer = strings.NewReplacer(charEscapeMapInvert...)
func invert(m []string) []string {
newM := make([]string, len(charEscapeMap))
for i := range m {
if (i + 1) % 2 == 0 {
newM[i] = m[i - 1]
newM[i - 1] = m[i]
}
}
return newM
}
func splitForFile(str string) []string {
split := []string{} split := []string{}
sb := &strings.Builder{} sb := &strings.Builder{}
quoted := false quoted := false
for _, r := range str { for i, r := range str {
if r == '"' { if r == '"' {
quoted = !quoted quoted = !quoted
sb.WriteRune(r) sb.WriteRune(r)
} else if r == ' ' && str[i - 1] == '\\' {
sb.WriteRune(r)
} else if !quoted && r == ' ' { } else if !quoted && r == ' ' {
split = append(split, sb.String()) split = append(split, sb.String())
sb.Reset() sb.Reset()
@ -39,12 +73,15 @@ func splitQuote(str string) []string {
} }
func fileComplete(query, ctx string, fields []string) ([]string, string) { func fileComplete(query, ctx string, fields []string) ([]string, string) {
q := splitQuote(ctx) q := splitForFile(ctx)
return matchPath(q[len(q) - 1]) return matchPath(q[len(q) - 1])
} }
func binaryComplete(query, ctx string, fields []string) ([]string, string) { func binaryComplete(query, ctx string, fields []string) ([]string, string) {
q := splitForFile(ctx)
query = q[len(q) - 1]
var completions []string var completions []string
prefixes := []string{"./", "../", "/", "~/"} prefixes := []string{"./", "../", "/", "~/"}
@ -54,7 +91,7 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) {
if len(fileCompletions) != 0 { if len(fileCompletions) != 0 {
for _, f := range fileCompletions { for _, f := range fileCompletions {
fullPath, _ := filepath.Abs(util.ExpandHome(query + strings.TrimPrefix(f, filePref))) fullPath, _ := filepath.Abs(util.ExpandHome(query + strings.TrimPrefix(f, filePref)))
if err := findExecutable(fullPath, false, true); err != nil { if err := findExecutable(escapeInvertReplaer.Replace(fullPath), false, true); err != nil {
continue continue
} }
completions = append(completions, f) completions = append(completions, f)
@ -66,7 +103,6 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) {
// filter out executables, but in path // filter out executables, but in path
for _, dir := range filepath.SplitList(os.Getenv("PATH")) { for _, dir := range filepath.SplitList(os.Getenv("PATH")) {
// print dir to stderr for debugging
// search for an executable which matches our query string // search for an executable which matches our query string
if matches, err := filepath.Glob(filepath.Join(dir, query + "*")); err == nil { if matches, err := filepath.Glob(filepath.Join(dir, query + "*")); err == nil {
// get basename from matches // get basename from matches
@ -102,6 +138,7 @@ func matchPath(query string) ([]string, string) {
var entries []string var entries []string
var baseName string var baseName string
query = escapeInvertReplaer.Replace(query)
path, _ := filepath.Abs(util.ExpandHome(filepath.Dir(query))) path, _ := filepath.Abs(util.ExpandHome(filepath.Dir(query)))
if string(query) == "" { if string(query) == "" {
// filepath base below would give us "." // filepath base below would give us "."
@ -129,25 +166,7 @@ func matchPath(query string) ([]string, string) {
} }
func escapeFilename(fname string) string { func escapeFilename(fname string) string {
args := []string{ return escapeReplaer.Replace(fname)
"\"", "\\\"",
"'", "\\'",
"`", "\\`",
" ", "\\ ",
"(", "\\(",
")", "\\)",
"[", "\\[",
"]", "\\]",
"$", "\\$",
"&", "\\&",
"*", "\\*",
">", "\\>",
"<", "\\<",
"|", "\\|",
}
r := strings.NewReplacer(args...)
return r.Replace(fname)
} }
func completionLoader(rtm *rt.Runtime) *rt.Table { func completionLoader(rtm *rt.Runtime) *rt.Table {

View File

@ -5,3 +5,5 @@
+ `hilbish.vimAction` -> actionName, args > Sent when the user does a "vim action," being something + `hilbish.vimAction` -> actionName, args > Sent when the user does a "vim action," being something
like yanking or pasting text. See `doc vim-mode actions` for more info. like yanking or pasting text. See `doc vim-mode actions` for more info.
+ `hilbish.cancel` > Sent when the user cancels their input with Ctrl-C.

View File

@ -181,11 +181,14 @@ input:
break break
} }
if err != nil { if err != nil {
if err != readline.CtrlC { if err == readline.CtrlC {
fmt.Println("^C")
hooks.Emit("hilbish.cancel")
} else {
// If we get a completely random error, print // If we get a completely random error, print
fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, err)
} }
fmt.Println("^C") // TODO: Halt if any other error occurs
continue continue
} }
var priv bool var priv bool

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"github.com/rivo/uniseg"
) )
// initGrid - Grid display details. Called each time we want to be sure to have // initGrid - Grid display details. Called each time we want to be sure to have
@ -13,8 +14,8 @@ func (g *CompletionGroup) initGrid(rl *Instance) {
// Compute size of each completion item box // Compute size of each completion item box
tcMaxLength := 1 tcMaxLength := 1
for i := range g.Suggestions { for i := range g.Suggestions {
if len(g.Suggestions[i]) > tcMaxLength { if uniseg.GraphemeClusterCount(g.Suggestions[i]) > tcMaxLength {
tcMaxLength = len([]rune(g.Suggestions[i])) tcMaxLength = uniseg.GraphemeClusterCount(g.Suggestions[i])
} }
} }
@ -103,7 +104,7 @@ func (g *CompletionGroup) writeGrid(rl *Instance) (comp string) {
rl.tcUsedY++ rl.tcUsedY++
} }
cellWidth := strconv.Itoa((GetTermWidth() / g.tcMaxX) - 2) cellWidth := strconv.Itoa((GetTermWidth() / g.tcMaxX) - 4)
x := 0 x := 0
y := 1 y := 1
@ -124,7 +125,15 @@ func (g *CompletionGroup) writeGrid(rl *Instance) (comp string) {
comp += seqInvert comp += seqInvert
} }
comp += fmt.Sprintf("%-"+cellWidth+"s %s", fmtEscape(g.Suggestions[i]), seqReset) sugg := g.Suggestions[i]
if len(sugg) > GetTermWidth() {
sugg = sugg[:GetTermWidth() - 4] + "..."
}
formatStr := "%-"+cellWidth+"s%s "
if g.tcMaxX == 1 {
formatStr = "%s%s"
}
comp += fmt.Sprintf(formatStr, fmtEscape(sugg), seqReset)
} }
// Always add a newline to the group if the end if not punctuated with one // Always add a newline to the group if the end if not punctuated with one

View File

@ -238,7 +238,9 @@ func (rl *Instance) Readline() (string, error) {
// Normal completion search does only refresh the search pattern and the comps // Normal completion search does only refresh the search pattern and the comps
if rl.modeTabFind || rl.modeAutoFind { if rl.modeTabFind || rl.modeAutoFind {
rl.resetVirtualComp(false)
rl.backspaceTabFind() rl.backspaceTabFind()
rl.renderHelpers()
rl.viUndoSkipAppend = true rl.viUndoSkipAppend = true
} else { } else {
// Always cancel any virtual completion // Always cancel any virtual completion
@ -331,6 +333,8 @@ func (rl *Instance) Readline() (string, error) {
rl.modeTabFind = true rl.modeTabFind = true
rl.updateTabFind([]rune{}) rl.updateTabFind([]rune{})
rl.updateVirtualComp()
rl.renderHelpers()
rl.viUndoSkipAppend = true rl.viUndoSkipAppend = true
// Tab Completion & Completion Search --------------------------------------------------------------- // Tab Completion & Completion Search ---------------------------------------------------------------
@ -484,7 +488,10 @@ func (rl *Instance) Readline() (string, error) {
if string(r[:i]) != seqShiftTab && if string(r[:i]) != seqShiftTab &&
string(r[:i]) != seqForwards && string(r[:i]) != seqBackwards && string(r[:i]) != seqForwards && string(r[:i]) != seqBackwards &&
string(r[:i]) != seqUp && string(r[:i]) != seqDown { string(r[:i]) != seqUp && string(r[:i]) != seqDown {
rl.resetVirtualComp(false) // basically only applies except on 1st ctrl r open
// so if we have not explicitly selected something
// (tabCompletionSelect is false) drop virtual completion
rl.resetVirtualComp(!rl.tabCompletionSelect)
} }
} }
@ -517,7 +524,9 @@ func (rl *Instance) Readline() (string, error) {
if rl.modeAutoFind || rl.modeTabFind { if rl.modeAutoFind || rl.modeTabFind {
rl.resetVirtualComp(false) rl.resetVirtualComp(false)
rl.updateTabFind(r[:i]) rl.updateTabFind(r[:i])
rl.renderHelpers()
rl.viUndoSkipAppend = true rl.viUndoSkipAppend = true
continue
} else { } else {
rl.resetVirtualComp(false) rl.resetVirtualComp(false)
rl.editorInput(r[:i]) rl.editorInput(r[:i])
@ -604,6 +613,7 @@ func (rl *Instance) escapeSeq(r []rune) {
case string(charEscape): case string(charEscape):
switch { switch {
case rl.modeAutoFind: case rl.modeAutoFind:
rl.resetVirtualComp(true)
rl.resetTabFind() rl.resetTabFind()
rl.clearHelpers() rl.clearHelpers()
rl.resetTabCompletion() rl.resetTabCompletion()
@ -611,6 +621,7 @@ func (rl *Instance) escapeSeq(r []rune) {
rl.renderHelpers() rl.renderHelpers()
case rl.modeTabFind: case rl.modeTabFind:
rl.resetVirtualComp(true)
rl.resetTabFind() rl.resetTabFind()
rl.resetTabCompletion() rl.resetTabCompletion()