Forge
csharp72c0d4fb
1namespace HybridCodebaseIndex.Core;
2
3using System.Text;
4
5internal static class WorkspaceScanner
6{
7 internal static IEnumerable<string> EnumerateIndexableFiles(
8 string rootDirectory,
9 IReadOnlySet<string> extensions,
10 IReadOnlyList<string> excludePathSegments,
11 IReadOnlyList<string> excludeRootFullPaths)
12 {
13 var normalizedRoot = Path.GetFullPath(rootDirectory.TrimEnd(Path.DirectorySeparatorChar));
14 if (!Directory.Exists(normalizedRoot))
15 yield break;
16
17 // Single-pass traversal (directory stack) to avoid N passes per extension.
18 var stack = new Stack<string>();
19 stack.Push(normalizedRoot);
20
21 while (stack.Count > 0)
22 {
23 var dir = stack.Pop();
24
25 if (ShouldExcludeDirectory(dir, excludePathSegments, excludeRootFullPaths))
26 continue;
27
28 IEnumerable<string> subDirs;
29 try
30 {
31 subDirs = Directory.EnumerateDirectories(dir);
32 }
33 catch
34 {
35 continue;
36 }
37
38 foreach (var sd in subDirs)
39 stack.Push(sd);
40
41 IEnumerable<string> files;
42 try
43 {
44 files = Directory.EnumerateFiles(dir);
45 }
46 catch
47 {
48 continue;
49 }
50
51 foreach (var f in files)
52 {
53 var ext = Path.GetExtension(f).ToLowerInvariant();
54 if (ext.Length == 0)
55 continue;
56 if (extensions.Contains(ext))
57 yield return f;
58 }
59 }
60 }
61
62 internal static bool ShouldExcludePath(string fullPath, IReadOnlyList<string> excludePathSegments)
63 {
64 if (excludePathSegments.Count == 0)
65 return false;
66
67 // Normalize for segment matching.
68 // Use directory separators to avoid accidental substring matches.
69 foreach (var seg0 in excludePathSegments)
70 {
71 var seg = seg0?.Trim();
72 if (string.IsNullOrEmpty(seg))
73 continue;
74
75 var token = $"{Path.DirectorySeparatorChar}{seg}{Path.DirectorySeparatorChar}";
76 if (fullPath.Contains(token, StringComparison.OrdinalIgnoreCase))
77 return true;
78 }
79
80 return false;
81 }
82
83 private static bool ShouldExcludeDirectory(
84 string fullPath,
85 IReadOnlyList<string> excludePathSegments,
86 IReadOnlyList<string> excludeRootFullPaths)
87 {
88 foreach (var root in excludeRootFullPaths)
89 {
90 if (fullPath.StartsWith(root, StringComparison.OrdinalIgnoreCase))
91 return true;
92 }
93
94 if (excludePathSegments.Count == 0)
95 return false;
96
97 // Quick check by directory name first.
98 var name = Path.GetFileName(fullPath);
99 if (!string.IsNullOrEmpty(name))
100 {
101 foreach (var seg0 in excludePathSegments)
102 {
103 if (string.Equals(name, seg0?.Trim(), StringComparison.OrdinalIgnoreCase))
104 return true;
105 }
106 }
107
108 return false;
109 }
110
111 internal static bool LooksBinary(ReadOnlySpan<byte> probe)
112 {
113 foreach (var b in probe)
114 {
115 if (b == 0)
116 return true;
117 }
118
119 return false;
120 }
121
122 internal static string RelativePath(string workspaceRoot, string filePath)
123 {
124 workspaceRoot = Path.GetFullPath(workspaceRoot.TrimEnd(Path.DirectorySeparatorChar));
125 filePath = Path.GetFullPath(filePath);
126 return Path.GetRelativePath(workspaceRoot, filePath);
127 }
128
129 internal static IEnumerable<(int lineStart, int lineEnd, string body)> ChunkByLines(
130 string text,
131 int chunkLines,
132 int overlapLines)
133 {
134 if (string.IsNullOrEmpty(text))
135 yield break;
136
137 // Normalize line endings so line accounting is stable.
138 var normalized = text.Replace("\r\n", "\n", StringComparison.Ordinal).Replace('\r', '\n');
139 var lines = normalized.Split('\n');
140 if (lines.Length == 0)
141 yield break;
142
143 var chunk = Math.Max(20, chunkLines);
144 var overlap = Math.Clamp(overlapLines, 0, chunk - 1);
145 var step = Math.Max(1, chunk - overlap);
146
147 for (var i = 0; i < lines.Length; i += step)
148 {
149 var endExclusive = Math.Min(lines.Length, i + chunk);
150 var sb = new StringBuilder(capacity: 4096);
151 for (var j = i; j < endExclusive; j++)
152 {
153 if (j > i) sb.Append('\n');
154 sb.Append(lines[j]);
155 }
156
157 // 1-based inclusive line numbers.
158 yield return (i + 1, endExclusive, sb.ToString());
159
160 if (endExclusive == lines.Length)
161 yield break;
162 }
163 }
164}
165
View only · write via MCP/CIDE