Skip to content
Open
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
50 changes: 48 additions & 2 deletions internal/buffer/autocomplete.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,56 @@ func (b *Buffer) GetArg() (string, int) {
return input, argstart
}

// fileArgEscapeChars are the characters that are escaped with a backslash in
// filename completions, so that commands receive the name as a single argument
const fileArgEscapeChars = " \t\"'"

// getFileArg is the same as GetArg, but a backslash-escaped space or quote
// does not end the argument. The escapes are removed from the returned string.
func (b *Buffer) getFileArg() (string, int) {
c := b.GetActiveCursor()
l := util.SliceStart(b.LineBytes(c.Y), c.X)

var input []byte
argstart := 0
escaped := false
for i := 0; len(l) > 0; i++ {
r, _, size := util.DecodeCharacter(l)
char := l[:size]
l = l[size:]

if escaped {
escaped = false
input = append(input, char...)
} else if r == ' ' {
input = input[:0]
argstart = i + 1
} else if r == '\\' && len(l) > 0 && strings.IndexByte(fileArgEscapeChars, l[0]) >= 0 {
escaped = true
} else {
input = append(input, char...)
}
}

return string(input), argstart
}

// escapeFileArg escapes the characters in fileArgEscapeChars with a backslash
func escapeFileArg(s string) string {
var buf strings.Builder
for _, r := range s {
if strings.ContainsRune(fileArgEscapeChars, r) {
buf.WriteByte('\\')
}
buf.WriteRune(r)
}
return buf.String()
}

// FileComplete autocompletes filenames
func FileComplete(b *Buffer) ([]string, []string) {
c := b.GetActiveCursor()
input, argstart := b.GetArg()
input, argstart := b.getFileArg()

sep := string(os.PathSeparator)
dirs := strings.Split(input, sep)
Expand Down Expand Up @@ -180,7 +226,7 @@ func FileComplete(b *Buffer) ([]string, []string) {
} else {
complete = suggestions[i]
}
completions[i] = util.SliceEndStr(complete, c.X-argstart)
completions[i] = util.SliceEndStr(escapeFileArg(complete), c.X-argstart)
}

return completions, suggestions
Expand Down
36 changes: 36 additions & 0 deletions internal/buffer/autocomplete_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package buffer

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
)

func fileComplete(input string) ([]string, []string) {
b := NewBufferFromString(input, "", BTInfo)
c := b.GetActiveCursor()
c.GotoLoc(b.End())
return FileComplete(b)
}

func TestFileCompleteEscapesSpecialChars(t *testing.T) {
dir := t.TempDir()
sep := string(os.PathSeparator)
assert.NoError(t, os.Mkdir(filepath.Join(dir, "my folder"), 0755))
assert.NoError(t, os.WriteFile(filepath.Join(dir, "my folder", "it's a file.txt"), nil, 0644))

// the suggestion is the plain name, the completion is escaped
completions, suggestions := fileComplete("open " + dir + sep + "my")
assert.Equal(t, []string{"my folder" + sep}, suggestions)
assert.Equal(t, []string{`\ folder` + sep}, completions)

// an escaped space does not start a new argument
completions, suggestions = fileComplete("open " + dir + sep + `my\ folder` + sep + "it")
assert.Equal(t, []string{"it's a file.txt"}, suggestions)
assert.Equal(t, []string{`\'s\ a\ file.txt`}, completions)

completions, _ = fileComplete("open " + dir + sep + `my\ folder` + sep + `it\'s\ a`)
assert.Equal(t, []string{`\ file.txt`}, completions)
}