...
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
module codewalker
|
module codewalker
|
||||||
|
|
||||||
import arrays
|
import arrays
|
||||||
|
import os
|
||||||
|
import incubaid.herolib.core.pathlib
|
||||||
|
|
||||||
// Default ignore patterns based on .gitignore conventions
|
// Default ignore patterns based on .gitignore conventions
|
||||||
const default_gitignore = '
|
const default_gitignore = '
|
||||||
@@ -48,8 +50,63 @@ Thumbs.db
|
|||||||
*.log
|
*.log
|
||||||
'
|
'
|
||||||
|
|
||||||
pub fn find_ignore_patterns() []string {
|
// find_ignore_patterns collects all .gitignore patterns from current directory up to repository root
|
||||||
|
//
|
||||||
|
// Walks up the directory tree using parent_find_advanced to locate all .gitignore files,
|
||||||
|
// stopping when it encounters the .git directory (repository root).
|
||||||
|
// Patterns are collected from:
|
||||||
|
// 1. Default ignore patterns (built-in)
|
||||||
|
// 2. All .gitignore files found from current directory to repository root
|
||||||
|
// 3. Filter out comments (lines starting with '#') and empty lines
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - start_path: Optional starting directory path (defaults to current working directory if empty)
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - Combined, sorted, unique ignore patterns from all sources
|
||||||
|
// - Error if path operations fail (file not found, permission denied, etc.)
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
// // Use current working directory
|
||||||
|
// patterns := find_ignore_patterns()!
|
||||||
|
//
|
||||||
|
// // Use specific project directory
|
||||||
|
// patterns := find_ignore_patterns('/home/user/myproject')!
|
||||||
|
pub fn find_ignore_patterns(start_path string) ![]string {
|
||||||
mut patterns := default_gitignore.split_into_lines()
|
mut patterns := default_gitignore.split_into_lines()
|
||||||
|
|
||||||
|
// Use provided path or current working directory
|
||||||
|
mut search_from := start_path
|
||||||
|
if search_from == '' { // If an empty string was passed for start_path, use current working directory
|
||||||
|
search_from = os.getwd()
|
||||||
|
}
|
||||||
|
|
||||||
|
mut current_path := pathlib.get(search_from)
|
||||||
|
|
||||||
|
// Find all .gitignore files up the tree until we hit .git directory (repo root)
|
||||||
|
mut gitignore_paths := current_path.parent_find_advanced('.gitignore', '.git')!
|
||||||
|
|
||||||
|
// Read and collect patterns from all found .gitignore files
|
||||||
|
for mut gitignore_path in gitignore_paths {
|
||||||
|
if gitignore_path.is_file() {
|
||||||
|
content := gitignore_path.read() or {
|
||||||
|
// Skip files that can't be read (permission issues, etc.)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
gitignore_lines := content.split_into_lines()
|
||||||
|
for line in gitignore_lines {
|
||||||
|
trimmed := line.trim_space()
|
||||||
|
|
||||||
|
// Skip empty lines and comment lines
|
||||||
|
if trimmed != '' && !trimmed.starts_with('#') {
|
||||||
|
patterns << trimmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort and get unique patterns to remove duplicates
|
||||||
patterns.sort()
|
patterns.sort()
|
||||||
patterns = arrays.uniq(patterns)
|
patterns = arrays.uniq(patterns)
|
||||||
|
|
||||||
|
|||||||
@@ -13,25 +13,17 @@ fn filemap_get_from_path(path string, content_read bool) !FileMap {
|
|||||||
source: path
|
source: path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ignore_patterns := find_ignore_patterns(path)!
|
||||||
|
|
||||||
// List all files using pathlib with both default and custom ignore patterns
|
// List all files using pathlib with both default and custom ignore patterns
|
||||||
mut file_list := dir.list(
|
mut file_list := dir.list(
|
||||||
recursive: true
|
recursive: true
|
||||||
ignore_default: true
|
filter_ignore: ignore_patterns
|
||||||
regex_ignore: ignore_patterns
|
|
||||||
)!
|
)!
|
||||||
|
|
||||||
// Process files with additional scoped ignore checking
|
|
||||||
for mut file in file_list.paths {
|
for mut file in file_list.paths {
|
||||||
if file.is_file() {
|
if file.is_file() {
|
||||||
relpath := file.path_relative(path)!
|
relpath := file.path_relative(path)!
|
||||||
|
|
||||||
// Check scoped ignore patterns (from .gitignore/.heroignore in subdirectories)
|
|
||||||
if cw.scoped_ignore.is_ignored(relpath) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if content_read {
|
if content_read {
|
||||||
content := file.read()!
|
content := file.read()!
|
||||||
fm.content[relpath] = content
|
fm.content[relpath] = content
|
||||||
@@ -44,84 +36,8 @@ fn filemap_get_from_path(path string, content_read bool) !FileMap {
|
|||||||
return fm
|
return fm
|
||||||
}
|
}
|
||||||
|
|
||||||
// load_ignore_files reads .gitignore and .heroignore files and builds scoped patterns
|
|
||||||
fn (mut cw CodeWalker) load_ignore_files(root_path string) ! {
|
|
||||||
mut root := pathlib.get(root_path)
|
|
||||||
if !root.is_dir() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// List all files to find ignore files
|
|
||||||
mut all_files := root.list(
|
|
||||||
recursive: true
|
|
||||||
ignore_default: false
|
|
||||||
)!
|
|
||||||
|
|
||||||
for mut p in all_files.paths {
|
|
||||||
if p.is_file() {
|
|
||||||
name := p.name()
|
|
||||||
if name == '.gitignore' || name == '.heroignore' {
|
|
||||||
relpath := p.path_relative(root_path)!
|
|
||||||
// Get the directory containing this ignore file
|
|
||||||
mut scope := relpath
|
|
||||||
if scope.contains('/') {
|
|
||||||
scope = scope.all_before_last('/')
|
|
||||||
} else {
|
|
||||||
scope = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
content := p.read()!
|
|
||||||
cw.scoped_ignore.add_for_scope(scope, content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse_header robustly extracts block type and filename from header line
|
|
||||||
// Handles variable `=` count, spaces, and case-insensitivity
|
|
||||||
// Example: ` ===FILE: myfile.txt ===` → $(BlockKind.file, "myfile.txt")
|
|
||||||
fn parse_header(line string) !(BlockKind, string) {
|
|
||||||
cleaned := line.trim_space()
|
|
||||||
|
|
||||||
// Must have = and content
|
|
||||||
if !cleaned.contains('=') {
|
|
||||||
return BlockKind.end, ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strip leading and trailing = (any count), preserving spaces between
|
|
||||||
mut content := cleaned.trim_left('=').trim_space()
|
|
||||||
content = content.trim_right('=').trim_space()
|
|
||||||
|
|
||||||
if content.len == 0 {
|
|
||||||
return BlockKind.end, ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for END marker
|
|
||||||
if content.to_lower() == 'end' {
|
|
||||||
return BlockKind.end, ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse FILE or FILECHANGE
|
|
||||||
if content.contains(':') {
|
|
||||||
kind_str := content.all_before(':').to_lower().trim_space()
|
|
||||||
filename := content.all_after(':').trim_space()
|
|
||||||
|
|
||||||
if filename.len < 1 {
|
|
||||||
return error('Invalid filename: empty after colon')
|
|
||||||
}
|
|
||||||
|
|
||||||
match kind_str {
|
|
||||||
'file' { return BlockKind.file, filename }
|
|
||||||
'filechange' { return BlockKind.filechange, filename }
|
|
||||||
else { return BlockKind.end, '' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return BlockKind.end, ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// filemap_get_from_content parses FileMap from string with ===FILE:name=== format
|
// filemap_get_from_content parses FileMap from string with ===FILE:name=== format
|
||||||
fn (mut cw CodeWalker) filemap_get_from_content(content string) !FileMap {
|
fn filemap_get_from_content(content string) !FileMap {
|
||||||
mut fm := FileMap{}
|
mut fm := FileMap{}
|
||||||
|
|
||||||
mut current_kind := BlockKind.end
|
mut current_kind := BlockKind.end
|
||||||
44
lib/ai/codewalker/parser.v
Normal file
44
lib/ai/codewalker/parser.v
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
module codewalker
|
||||||
|
|
||||||
|
// parse_header robustly extracts block type and filename from header line
|
||||||
|
// Handles variable `=` count, spaces, and case-insensitivity
|
||||||
|
// Example: ` ===FILE: myfile.txt ===` → $(BlockKind.file, "myfile.txt")
|
||||||
|
fn parse_header(line string) !(BlockKind, string) {
|
||||||
|
cleaned := line.trim_space()
|
||||||
|
|
||||||
|
// Must have = and content
|
||||||
|
if !cleaned.contains('=') {
|
||||||
|
return BlockKind.end, ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip leading and trailing = (any count), preserving spaces between
|
||||||
|
mut content := cleaned.trim_left('=').trim_space()
|
||||||
|
content = content.trim_right('=').trim_space()
|
||||||
|
|
||||||
|
if content.len == 0 {
|
||||||
|
return BlockKind.end, ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for END marker
|
||||||
|
if content.to_lower() == 'end' {
|
||||||
|
return BlockKind.end, ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse FILE or FILECHANGE
|
||||||
|
if content.contains(':') {
|
||||||
|
kind_str := content.all_before(':').to_lower().trim_space()
|
||||||
|
filename := content.all_after(':').trim_space()
|
||||||
|
|
||||||
|
if filename.len < 1 {
|
||||||
|
return error('Invalid filename: empty after colon')
|
||||||
|
}
|
||||||
|
|
||||||
|
match kind_str {
|
||||||
|
'file' { return BlockKind.file, filename }
|
||||||
|
'filechange' { return BlockKind.filechange, filename }
|
||||||
|
else { return BlockKind.end, '' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return BlockKind.end, ''
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user