summaryrefslogtreecommitdiff
path: root/internal/ignore/checker.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-08 10:39:51 +0200
committerPaul Buetow <paul@buetow.org>2026-02-08 10:39:51 +0200
commitd5b13224737a9f66c3d5113a885603b32867d740 (patch)
treed482cc965a65be22604800fe6772279c52961b99 /internal/ignore/checker.go
parentbd698b257a548d835fbc2675ff5be5e1a69ff229 (diff)
add gitignore-aware file filtering for LSP completions and code actionsv0.18.0
Files matching .gitignore patterns or user-configured extra patterns are now skipped for completions and code actions. Configurable via [ignore] section in config.toml with gitignore, extra_patterns, and lsp_notify_ignored options. Includes hot-reload support and env var overrides (HEXAI_IGNORE_*). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/ignore/checker.go')
-rw-r--r--internal/ignore/checker.go90
1 files changed, 90 insertions, 0 deletions
diff --git a/internal/ignore/checker.go b/internal/ignore/checker.go
new file mode 100644
index 0000000..92129b8
--- /dev/null
+++ b/internal/ignore/checker.go
@@ -0,0 +1,90 @@
+// Summary: Thread-safe gitignore-aware file checker that combines .gitignore
+// patterns with user-configured extra patterns. Used by the LSP server to
+// skip completions and code actions for ignored files.
+package ignore
+
+import (
+ "path/filepath"
+ "strings"
+ "sync"
+
+ gitignore "github.com/sabhiram/go-gitignore"
+)
+
+// Checker evaluates whether an absolute file path should be ignored based on
+// .gitignore patterns and/or user-configured extra patterns. It is safe for
+// concurrent use.
+type Checker struct {
+ mu sync.RWMutex
+ gitRoot string
+ giMatcher *gitignore.GitIgnore // compiled .gitignore (nil when disabled or missing)
+ exMatcher *gitignore.GitIgnore // compiled extra patterns (nil when empty)
+}
+
+// New creates a Checker. If useGitignore is true and gitRoot is non-empty, it
+// loads .gitignore from gitRoot. extraPatterns are always compiled (gitignore
+// syntax).
+func New(gitRoot string, useGitignore bool, extraPatterns []string) *Checker {
+ c := &Checker{gitRoot: gitRoot}
+ c.compile(useGitignore, extraPatterns)
+ return c
+}
+
+// IsIgnored returns whether absPath should be ignored and a human-readable
+// reason string. When the checker is nil, nothing is ignored.
+func (c *Checker) IsIgnored(absPath string) (ignored bool, reason string) {
+ if c == nil {
+ return false, ""
+ }
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+
+ rel, inside := c.relPath(absPath)
+
+ // Only check gitignore when the path is inside the git root
+ if inside && c.giMatcher != nil && c.giMatcher.MatchesPath(rel) {
+ return true, "matched .gitignore pattern"
+ }
+ if c.exMatcher != nil && c.exMatcher.MatchesPath(rel) {
+ return true, "matched extra ignore pattern"
+ }
+ return false, ""
+}
+
+// Update recompiles matchers for hot-reload. Thread-safe.
+func (c *Checker) Update(useGitignore bool, extraPatterns []string) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ c.compile(useGitignore, extraPatterns)
+}
+
+// compile builds the gitignore and extra-pattern matchers. Must be called
+// under c.mu write lock (or during construction).
+func (c *Checker) compile(useGitignore bool, extraPatterns []string) {
+ c.giMatcher = nil
+ c.exMatcher = nil
+
+ if useGitignore && c.gitRoot != "" {
+ giPath := filepath.Join(c.gitRoot, ".gitignore")
+ if gi, err := gitignore.CompileIgnoreFile(giPath); err == nil {
+ c.giMatcher = gi
+ }
+ }
+ if len(extraPatterns) > 0 {
+ c.exMatcher = gitignore.CompileIgnoreLines(extraPatterns...)
+ }
+}
+
+// relPath converts absPath to a path relative to gitRoot. Returns the
+// relative path and true if the path is inside the git root; otherwise
+// returns the original path and false.
+func (c *Checker) relPath(absPath string) (string, bool) {
+ if c.gitRoot == "" {
+ return absPath, false
+ }
+ rel, err := filepath.Rel(c.gitRoot, absPath)
+ if err != nil || strings.HasPrefix(rel, "..") {
+ return absPath, false
+ }
+ return rel, true
+}