Repository: charmbracelet/crush (2025-08-14T04:14:59Z)

Issue:
File completion returns zero results in repositories with ".*" gitignore pattern
### Describe the bug

File completion (triggered by typing `/`) returns zero files in repositories that have the gitignore pattern `.*`, even though the repository contains many files. This affects large repositories like the Linux kernel and any project using `.*` to ignore
hidden files.

The root cause is that the `.*` gitignore pattern incorrectly matches the current directory `.` itself during directory traversal, causing `filepath.SkipDir` to skip the entire scan.

### Setup

Please complete the following information along with version numbers, if applicable.
- OS: Linux (Ubuntu, Arch, etc.) / macOS
- Shell: zsh, bash, fish
- Terminal Emulator: Any
- Terminal Multiplexer: Any

This bug affects generally all platforms and terminal configurations.

### To Reproduce

Steps to reproduce the behavior:
1. Clone a repository with `.*` in `.gitignore` (e.g., Linux kernel: `git clone https://github.com/torvalds/linux.git`)
2. Or create a test repository:
 ```bash
 mkdir test-repo && cd test-repo
 echo ".*" > .gitignore
 echo "test content" > example.txt
 echo "more content" > another.go
```
3. Or add `.*` to the crush repo
4. Run crush in that directory
5. Type / to trigger file completion
6. See that zero files are returned despite files existing in the directory


### Expected behavior

File completion should return all non-ignored files in the directory. The .* pattern should only ignore files starting with . (like .bashrc, .gitignore) but should not prevent scanning the current directory altogether.

Tools like git, ripgrep, and fd correctly handle this case and find files in repositories with .* patterns.





Discussion:

Patch:
diff --git a/internal/fsext/ls.go b/internal/fsext/ls.go
index e691c148c..b18e1ef6a 100644
--- a/internal/fsext/ls.go
+++ b/internal/fsext/ls.go
@@ -125,18 +125,24 @@ func (dl *directoryLister) shouldIgnore(path string, ignorePatterns []string) bo
 		}
 	}
 
+	// Don't apply gitignore rules to the root directory itself
+	// In gitignore semantics, patterns don't apply to the repo root
+	if path == dl.rootPath {
+		return false
+	}
+
 	relPath, err := filepath.Rel(dl.rootPath, path)
 	if err != nil {
 		relPath = path
 	}
 
 	if commonIgnorePatterns().MatchesPath(relPath) {
-		slog.Debug("ingoring common pattern", "path", relPath)
+		slog.Debug("ignoring common pattern", "path", relPath)
 		return true
 	}
 
 	if dl.getIgnore(filepath.Dir(path)).MatchesPath(relPath) {
-		slog.Debug("ingoring dir pattern", "path", relPath, "dir", filepath.Dir(path))
+		slog.Debug("ignoring dir pattern", "path", relPath, "dir", filepath.Dir(path))
 		return true
 	}
 
@@ -145,7 +151,7 @@ func (dl *directoryLister) shouldIgnore(path string, ignorePatterns []string) bo
 	}
 
 	if homeIgnore().MatchesPath(relPath) {
-		slog.Debug("ingoring home dir pattern", "path", relPath)
+		slog.Debug("ignoring home dir pattern", "path", relPath)
 		return true
 	}
 


Test patch:
diff --git a/internal/fsext/ls_test.go b/internal/fsext/ls_test.go
new file mode 100644
index 000000000..a74ca3276
--- /dev/null
+++ b/internal/fsext/ls_test.go
@@ -0,0 +1,66 @@
+package fsext
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func chdir(t *testing.T, dir string) {
+	original, err := os.Getwd()
+	require.NoError(t, err)
+
+	err = os.Chdir(dir)
+	require.NoError(t, err)
+
+	t.Cleanup(func() {
+		err := os.Chdir(original)
+		require.NoError(t, err)
+	})
+}
+
+func TestListDirectory(t *testing.T) {
+	tempDir := t.TempDir()
+	chdir(t, tempDir)
+
+	testFiles := map[string]string{
+		"regular.txt":     "content",
+		".hidden":         "hidden content",
+		".gitignore":      ".*\n*.log\n",
+		"subdir/file.go":  "package main",
+		"subdir/.another": "more hidden",
+		"build.log":       "build output",
+	}
+
+	for filePath, content := range testFiles {
+		dir := filepath.Dir(filePath)
+		if dir != "." {
+			require.NoError(t, os.MkdirAll(dir, 0o755))
+		}
+
+		err := os.WriteFile(filePath, []byte(content), 0o644)
+		require.NoError(t, err)
+	}
+
+	files, truncated, err := ListDirectory(".", nil, 0)
+	require.NoError(t, err)
+	assert.False(t, truncated)
+	assert.Equal(t, len(files), 4)
+
+	fileSet := make(map[string]bool)
+	for _, file := range files {
+		fileSet[filepath.ToSlash(file)] = true
+	}
+
+	assert.True(t, fileSet["./regular.txt"])
+	assert.True(t, fileSet["./subdir/"])
+	assert.True(t, fileSet["./subdir/file.go"])
+	assert.True(t, fileSet["./regular.txt"])
+
+	assert.False(t, fileSet["./.hidden"])
+	assert.False(t, fileSet["./.gitignore"])
+	assert.False(t, fileSet["./build.log"])
+}