Work on adding ignorefiles support to choose-files
This commit is contained in:
parent
a341f8b56f
commit
351275cb8c
5 changed files with 200 additions and 19 deletions
|
|
@ -74,10 +74,11 @@ func (c *ResultCollection) Batch(offset *CollectionIndex) (ans []ResultItem) {
|
|||
return
|
||||
}
|
||||
|
||||
func (c *ResultCollection) NextDir(offset *CollectionIndex) (ans string) {
|
||||
func (c *ResultCollection) NextDir(offset *CollectionIndex) (ans string, ignore_files []ignore_file_with_prefix) {
|
||||
for ans == "" && offset.Compare(c.append_idx) < 0 {
|
||||
if c.slices[offset.Slice][offset.Pos].ftype&fs.ModeDir != 0 {
|
||||
ans = c.slices[offset.Slice][offset.Pos].text
|
||||
ignore_files = c.slices[offset.Slice][offset.Pos].ignore_files
|
||||
}
|
||||
offset.Pos++
|
||||
if offset.Pos >= len(c.slices[offset.Slice]) {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import (
|
|||
"unicode/utf8"
|
||||
|
||||
"github.com/kovidgoyal/kitty/tools/fzf"
|
||||
"github.com/kovidgoyal/kitty/tools/ignorefiles"
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
|
@ -28,11 +29,23 @@ func (c CombinedScore) String() string {
|
|||
return fmt.Sprintf("{score: %d length: %d index: %d}", c.Score(), c.Length(), c.Index())
|
||||
}
|
||||
|
||||
type ignore_file_with_prefix struct {
|
||||
impl ignorefiles.IgnoreFile
|
||||
prefix string
|
||||
}
|
||||
|
||||
func (i *ignore_file_with_prefix) is_ignored(name string, ftype fs.FileMode) (ans bool, was_match bool) {
|
||||
ans, linenum, _ := i.impl.IsIgnored(i.prefix+name, ftype)
|
||||
was_match = linenum > -1
|
||||
return
|
||||
}
|
||||
|
||||
type ResultItem struct {
|
||||
text string
|
||||
ftype fs.FileMode
|
||||
positions []int // may be nil
|
||||
score CombinedScore
|
||||
text string
|
||||
ftype fs.FileMode
|
||||
positions []int // may be nil
|
||||
score CombinedScore
|
||||
ignore_files []ignore_file_with_prefix
|
||||
}
|
||||
type ResultsType []*ResultItem
|
||||
|
||||
|
|
@ -63,7 +76,9 @@ type FileSystemScanner struct {
|
|||
mutex sync.Mutex
|
||||
collection *ResultCollection
|
||||
dir_reader func(path string) ([]fs.DirEntry, error)
|
||||
file_reader func(path string) ([]byte, error)
|
||||
filter_func func(filename string) bool
|
||||
global_gitignore ignorefiles.IgnoreFile
|
||||
err error
|
||||
}
|
||||
|
||||
|
|
@ -72,7 +87,9 @@ func NewFileSystemScanner(root_dir string, notify chan bool, filter_func func(st
|
|||
ans.in_progress.Store(true)
|
||||
ans.keep_going.Store(true)
|
||||
ans.dir_reader = os.ReadDir
|
||||
ans.file_reader = os.ReadFile
|
||||
ans.filter_func = utils.IfElse(filter_func == nil, accept_all, filter_func)
|
||||
ans.global_gitignore = ignorefiles.GlobalGitignore()
|
||||
return ans
|
||||
}
|
||||
|
||||
|
|
@ -206,11 +223,17 @@ func (fss *FileSystemScanner) worker() {
|
|||
root_dir += string(os.PathSeparator)
|
||||
}
|
||||
dir := root_dir
|
||||
var ignore_files []ignore_file_with_prefix
|
||||
base := ""
|
||||
pos := &CollectionIndex{}
|
||||
var arena []sortable_dir_entry
|
||||
var sortable []*sortable_dir_entry
|
||||
var ignoreable []*sortable_dir_entry
|
||||
var idx uint32
|
||||
dot_git := os.Getenv("GIT_DIR")
|
||||
if dot_git == "" {
|
||||
dot_git = ".git"
|
||||
}
|
||||
// do a breadth first traversal of the filesystem
|
||||
is_root := true
|
||||
for dir != "" {
|
||||
|
|
@ -230,15 +253,59 @@ func (fss *FileSystemScanner) worker() {
|
|||
if cap(arena) < len(entries) {
|
||||
arena = make([]sortable_dir_entry, 0, max(1024, len(entries), 2*cap(arena)))
|
||||
sortable = make([]*sortable_dir_entry, 0, cap(arena))
|
||||
ignoreable = make([]*sortable_dir_entry, 0, cap(arena))
|
||||
}
|
||||
arena = arena[:len(entries)]
|
||||
sortable = sortable[:0]
|
||||
ignoreable = ignoreable[:0]
|
||||
ignore_files_copied := false
|
||||
add_ignore_file_from_impl := func(impl ignorefiles.IgnoreFile) {
|
||||
// we want ignore_files to be a copy as we dont want to
|
||||
// change the underlying array of ignore_files as it is
|
||||
// referenced by multiple ResultItems
|
||||
if !ignore_files_copied {
|
||||
ignore_files_copied = true
|
||||
n := make([]ignore_file_with_prefix, len(ignore_files), len(ignore_files)+4)
|
||||
copy(n, ignore_files)
|
||||
ignore_files = n
|
||||
}
|
||||
ignore_files = append(ignore_files, ignore_file_with_prefix{impl: impl})
|
||||
}
|
||||
add_ignore_file := func(name string) {
|
||||
if data, rerr := fss.file_reader(dir + string(os.PathSeparator) + name); rerr == nil {
|
||||
impl := ignorefiles.NewGitignore()
|
||||
if rerr = impl.LoadString(utils.UnsafeBytesToString(data)); rerr == nil {
|
||||
add_ignore_file_from_impl(impl)
|
||||
}
|
||||
}
|
||||
}
|
||||
entry_is_ignored := func(name string, ftype fs.FileMode) (is_ignored bool) {
|
||||
for _, ignore_file := range ignore_files {
|
||||
if iig, was_match := ignore_file.is_ignored(name, ftype); was_match {
|
||||
is_ignored = iig
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
has_git_ignore, has_dot_git, has_dot_ignore := false, false, false
|
||||
for i, e := range entries {
|
||||
name := e.Name()
|
||||
ftype := e.Type()
|
||||
is_dir := ftype&fs.ModeDir != 0
|
||||
if !is_dir && !fss.filter_func(name) {
|
||||
continue
|
||||
if !is_dir {
|
||||
switch name {
|
||||
case ".ignore":
|
||||
has_git_ignore = false
|
||||
case ".gitignore":
|
||||
has_git_ignore = true
|
||||
}
|
||||
if !fss.filter_func(name) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if name == dot_git {
|
||||
has_dot_git = true
|
||||
}
|
||||
}
|
||||
arena[i].name = name
|
||||
if ftype&fs.ModeSymlink != 0 {
|
||||
|
|
@ -256,13 +323,36 @@ func (fss *FileSystemScanner) worker() {
|
|||
arena[i].sort_key = arena[i].buf[:1+n]
|
||||
sortable = append(sortable, &arena[i])
|
||||
}
|
||||
slices.SortFunc(sortable, func(a, b *sortable_dir_entry) int { return bytes.Compare(a.sort_key, b.sort_key) })
|
||||
if has_dot_git {
|
||||
if fss.global_gitignore != nil {
|
||||
add_ignore_file_from_impl(fss.global_gitignore)
|
||||
}
|
||||
add_ignore_file(filepath.Join(dot_git, "info", "exclude"))
|
||||
if has_git_ignore {
|
||||
add_ignore_file(".gitignore")
|
||||
}
|
||||
}
|
||||
if has_dot_ignore {
|
||||
add_ignore_file(".ignore")
|
||||
}
|
||||
final_entries := sortable
|
||||
if len(ignore_files) > 0 {
|
||||
for _, e := range sortable {
|
||||
if !entry_is_ignored(e.name, e.ftype) {
|
||||
ignoreable = append(ignoreable, e)
|
||||
}
|
||||
|
||||
}
|
||||
final_entries = ignoreable
|
||||
}
|
||||
slices.SortFunc(final_entries, func(a, b *sortable_dir_entry) int { return bytes.Compare(a.sort_key, b.sort_key) })
|
||||
fss.lock()
|
||||
for _, e := range sortable {
|
||||
for _, e := range final_entries {
|
||||
i := fss.collection.NextAppendPointer()
|
||||
i.ftype = e.ftype
|
||||
i.text = base + e.name
|
||||
i.score.Set_index(idx)
|
||||
i.ignore_files = ignore_files
|
||||
idx++
|
||||
}
|
||||
listeners := fss.listeners
|
||||
|
|
@ -273,9 +363,16 @@ func (fss *FileSystemScanner) worker() {
|
|||
default:
|
||||
}
|
||||
}
|
||||
if relpath := fss.collection.NextDir(pos); relpath != "" {
|
||||
ignore_files = nil
|
||||
if relpath, ignf := fss.collection.NextDir(pos); relpath != "" {
|
||||
base = relpath + string(os.PathSeparator)
|
||||
dir = root_dir + base
|
||||
if len(ignf) != 0 {
|
||||
name := filepath.Base(relpath) + string(os.PathSeparator)
|
||||
ignore_files = utils.Map(func(ignore_file ignore_file_with_prefix) ignore_file_with_prefix {
|
||||
return ignore_file_with_prefix{impl: ignore_file.impl, prefix: ignore_file.prefix + name}
|
||||
}, ignf)
|
||||
}
|
||||
} else {
|
||||
dir = ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ func TestAsLower(t *testing.T) {
|
|||
type node struct {
|
||||
name string
|
||||
children map[string]*node
|
||||
data string
|
||||
}
|
||||
|
||||
func (n node) Name() string {
|
||||
|
|
@ -87,6 +88,21 @@ func (n node) dir_entries() []fs.DirEntry {
|
|||
return entries
|
||||
}
|
||||
|
||||
func (n node) ReadFile(name string) ([]byte, error) {
|
||||
if name == string(os.PathSeparator) {
|
||||
return nil, fs.ErrNotExist
|
||||
}
|
||||
p := &n
|
||||
for _, x := range strings.Split(strings.Trim(name, string(os.PathSeparator)), string(os.PathSeparator)) {
|
||||
c, found := p.children[x]
|
||||
if !found || c.IsDir() {
|
||||
return nil, fs.ErrNotExist
|
||||
}
|
||||
p = c
|
||||
}
|
||||
return []byte(p.data), nil
|
||||
}
|
||||
|
||||
func (n node) ReadDir(name string) ([]fs.DirEntry, error) {
|
||||
if name == string(os.PathSeparator) {
|
||||
return n.dir_entries(), nil
|
||||
|
|
@ -105,19 +121,38 @@ func (n node) ReadDir(name string) ([]fs.DirEntry, error) {
|
|||
return p.dir_entries(), nil
|
||||
}
|
||||
|
||||
func TestChooseFilesIgnore(t *testing.T) {
|
||||
root := node{name: string(os.PathSeparator), children: map[string]*node{
|
||||
"b": {name: "b"},
|
||||
"a": {name: "a"},
|
||||
"c.png": {name: "c.png"},
|
||||
".ignore": {name: ".ignore", data: "a\nx/s/n"},
|
||||
"x": {name: "x", children: map[string]*node{
|
||||
"1": {name: "1"}, "2": {name: "2"}, "3": {name: "3"},
|
||||
"s": {name: "s", children: map[string]*node{
|
||||
"m": {name: "m"}, "n": {name: "n"},
|
||||
}},
|
||||
}},
|
||||
"y": {name: "y", children: map[string]*node{
|
||||
"3": {name: "3"}, "4": {name: "4"}, "5": {name: "5"},
|
||||
}},
|
||||
}}
|
||||
_ = root
|
||||
}
|
||||
|
||||
func TestChooseFilesScoring(t *testing.T) {
|
||||
root := node{name: string(os.PathSeparator), children: map[string]*node{
|
||||
"b": {name: "b"},
|
||||
"a": {name: "a"},
|
||||
"c.png": {name: "c.png"},
|
||||
"x": {name: "x", children: map[string]*node{
|
||||
"1": {"1", nil}, "2": {"2", nil}, "3": {"3", nil},
|
||||
"s": {"s", map[string]*node{
|
||||
"m": {"m", nil}, "n": {"n", nil},
|
||||
"1": {name: "1"}, "2": {name: "2"}, "3": {name: "3"},
|
||||
"s": {name: "s", children: map[string]*node{
|
||||
"m": {name: "m"}, "n": {name: "n"},
|
||||
}},
|
||||
}},
|
||||
"y": {name: "y", children: map[string]*node{
|
||||
"3": {"3", nil}, "4": {"4", nil}, "5": {"5", nil},
|
||||
"3": {name: "3"}, "4": {name: "4"}, "5": {name: "5"},
|
||||
}},
|
||||
}}
|
||||
wg := sync.WaitGroup{}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
|
@ -16,10 +17,15 @@ type IgnoreFile interface {
|
|||
LoadPath(string) error
|
||||
|
||||
// relpath is the path relative to the directory containing the ignorefile.
|
||||
// When is_ignored is true, linenum_of_matching_rule will be the line
|
||||
// number of the rule causing relpath to be ignored and pattern is the
|
||||
// textual representation of the matching pattern.
|
||||
// When the result is due to a rule matching, linenum_of_matching_rule is
|
||||
// >=0 and pattern is the textual representation of the rule. Otherwise
|
||||
// linenum_of_matching_rule is -1 and pattern is the empty string.
|
||||
IsIgnored(relpath string, ftype fs.FileMode) (is_ignored bool, linenum_of_matching_rule int, pattern string)
|
||||
}
|
||||
|
||||
func NewGitignore() IgnoreFile { return &Gitignore{index_of_last_negated_rule: -1} }
|
||||
|
||||
// The global gitignore from ~/.config/git/ignore
|
||||
var GlobalGitignore = sync.OnceValue(func() IgnoreFile {
|
||||
return get_global_gitignore()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -41,8 +41,8 @@ func (g Gitignore) IsIgnored(relpath string, ftype os.FileMode) (is_ignored bool
|
|||
}
|
||||
if pat.negated && pat.Match(relpath, ftype) {
|
||||
is_ignored = false
|
||||
linenum_of_matching_rule = -1
|
||||
pattern = ""
|
||||
linenum_of_matching_rule = pat.line_number
|
||||
pattern = pat.pattern
|
||||
}
|
||||
} else {
|
||||
if !pat.negated && pat.Match(relpath, ftype) {
|
||||
|
|
@ -257,3 +257,45 @@ func CompileGitIgnoreLine(line string) (ans GitPattern, skipped_line bool) {
|
|||
}
|
||||
return
|
||||
}
|
||||
|
||||
func get_global_gitconfig_excludesfile() (ans string) {
|
||||
cfhome := os.Getenv("XDG_CONFIG_HOME")
|
||||
if cfhome == "" {
|
||||
cfhome = utils.Expanduser("~/.config")
|
||||
}
|
||||
for _, candidate := range []string{"/etc/gitconfig", filepath.Join(cfhome, "git", "config"), utils.Expanduser("~/.gitconfig")} {
|
||||
if data, err := os.ReadFile(candidate); err == nil {
|
||||
s := utils.NewLineScanner(utils.UnsafeBytesToString(data))
|
||||
in_core := false
|
||||
for s.Scan() {
|
||||
line := strings.TrimSpace(s.Text())
|
||||
if in_core {
|
||||
if strings.HasPrefix(line, "[") {
|
||||
in_core = false
|
||||
continue
|
||||
}
|
||||
if k, rest, found := strings.Cut(line, "="); found && strings.ToLower(strings.TrimSpace(k)) == `excludesfile` {
|
||||
ans = strings.TrimSpace(rest)
|
||||
}
|
||||
} else if strings.ToLower(line) == "[core]" {
|
||||
in_core = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ans == "" {
|
||||
ans = filepath.Join(cfhome, "git", "ignore")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func get_global_gitignore() (ans IgnoreFile) {
|
||||
excludesfile := get_global_gitconfig_excludesfile()
|
||||
if data, err := os.ReadFile(excludesfile); err == nil {
|
||||
q := NewGitignore()
|
||||
if q.LoadString(utils.UnsafeBytesToString(data)) == nil {
|
||||
ans = q
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue