feat: implement ignore patterns and extension filters in CodexLens

- Added tests to ensure loading of ignore patterns and extension filters from settings.
- Implemented functionality to respect ignore patterns and extension filters during file indexing.
- Created integration tests for CodexLens ignore-pattern configuration routes.
- Added a new AdvancedTab component with tests for managing ignore patterns and extension filters.
- Established a comprehensive branding naming system for the Maestro project, including guidelines for package names, CLI commands, and directory structure.
This commit is contained in:
catlog22
2026-03-09 14:43:21 +08:00
parent 3341a2e772
commit b2fc2f60f1
33 changed files with 1489 additions and 69 deletions

View File

@@ -0,0 +1 @@
{"ignore_patterns": ["frontend/dist"], "extension_filters": ["*.min.js"]}

View File

@@ -0,0 +1 @@
export const app = 1

View File

@@ -0,0 +1 @@
export const bundle = 1

View File

@@ -0,0 +1 @@
export const compiled = 1

View File

@@ -0,0 +1 @@
export const bundle = 1

View File

@@ -0,0 +1 @@
export const app = 1

View File

@@ -0,0 +1 @@
print('artifact')

View File

@@ -0,0 +1 @@
print('artifact')

View File

@@ -0,0 +1 @@
print('artifact')

View File

@@ -0,0 +1 @@
print('artifact')

View File

@@ -0,0 +1 @@
print('artifact')

View File

@@ -0,0 +1 @@
print('artifact')

View File

@@ -0,0 +1 @@
print('artifact')

View File

@@ -0,0 +1 @@
print('ok')

View File

@@ -0,0 +1 @@
print('artifact')

View File

@@ -0,0 +1 @@
export const app = 1

View File

@@ -0,0 +1 @@
export const bundle = 1

View File

@@ -0,0 +1 @@
export const skip = 1

View File

@@ -0,0 +1 @@
{"ignore_patterns": ["frontend/dist", "coverage"], "extension_filters": ["*.min.js", "*.map"]}

View File

@@ -0,0 +1 @@
print('compiled')

View File

@@ -123,11 +123,12 @@ class IndexTreeBuilder:
"""
self.registry = registry
self.mapper = mapper
self.config = config or Config()
self.config = config or Config.load()
self.parser_factory = ParserFactory(self.config)
self.logger = logging.getLogger(__name__)
self.incremental = incremental
self.ignore_patterns = self._resolve_ignore_patterns()
self.extension_filters = self._resolve_extension_filters()
def _resolve_ignore_patterns(self) -> Tuple[str, ...]:
configured_patterns = getattr(self.config, "ignore_patterns", None)
@@ -139,6 +140,18 @@ class IndexTreeBuilder:
cleaned.append(pattern)
return tuple(dict.fromkeys(cleaned))
def _resolve_extension_filters(self) -> Tuple[str, ...]:
configured_filters = getattr(self.config, "extension_filters", None)
if not configured_filters:
return tuple()
cleaned: List[str] = []
for item in configured_filters:
pattern = str(item).strip().replace('\\', '/').rstrip('/')
if pattern:
cleaned.append(pattern)
return tuple(dict.fromkeys(cleaned))
def _is_ignored_dir(self, dir_path: Path, source_root: Optional[Path] = None) -> bool:
name = dir_path.name
if name.startswith('.'):
@@ -159,6 +172,25 @@ class IndexTreeBuilder:
return False
def _is_filtered_file(self, file_path: Path, source_root: Optional[Path] = None) -> bool:
if not self.extension_filters:
return False
rel_path: Optional[str] = None
if source_root is not None:
try:
rel_path = file_path.relative_to(source_root).as_posix()
except ValueError:
rel_path = None
for pattern in self.extension_filters:
if pattern == file_path.name or fnmatch.fnmatch(file_path.name, pattern):
return True
if rel_path and (pattern == rel_path or fnmatch.fnmatch(rel_path, pattern)):
return True
return False
def build(
self,
source_root: Path,
@@ -259,6 +291,7 @@ class IndexTreeBuilder:
dirs,
languages,
workers,
source_root=source_root,
project_id=project_info.id,
global_index_db_path=global_index_db_path,
)
@@ -410,6 +443,7 @@ class IndexTreeBuilder:
return self._build_single_dir(
source_path,
languages=None,
source_root=project_root,
project_id=project_info.id,
global_index_db_path=global_index_db_path,
)
@@ -491,7 +525,7 @@ class IndexTreeBuilder:
return False
# Check for supported files in this directory
source_files = self._iter_source_files(dir_path, languages)
source_files = self._iter_source_files(dir_path, languages, source_root=source_root)
if len(source_files) > 0:
return True
@@ -519,7 +553,7 @@ class IndexTreeBuilder:
True if directory tree contains indexable files
"""
# Check for supported files in this directory
source_files = self._iter_source_files(dir_path, languages)
source_files = self._iter_source_files(dir_path, languages, source_root=source_root)
if len(source_files) > 0:
return True
@@ -543,6 +577,7 @@ class IndexTreeBuilder:
languages: List[str],
workers: int,
*,
source_root: Path,
project_id: int,
global_index_db_path: Path,
) -> List[DirBuildResult]:
@@ -570,6 +605,7 @@ class IndexTreeBuilder:
result = self._build_single_dir(
dirs[0],
languages,
source_root=source_root,
project_id=project_id,
global_index_db_path=global_index_db_path,
)
@@ -585,6 +621,7 @@ class IndexTreeBuilder:
"static_graph_relationship_types": self.config.static_graph_relationship_types,
"use_astgrep": getattr(self.config, "use_astgrep", False),
"ignore_patterns": list(getattr(self.config, "ignore_patterns", [])),
"extension_filters": list(getattr(self.config, "extension_filters", [])),
}
worker_args = [
@@ -595,6 +632,7 @@ class IndexTreeBuilder:
config_dict,
int(project_id),
str(global_index_db_path),
str(source_root),
)
for dir_path in dirs
]
@@ -631,6 +669,7 @@ class IndexTreeBuilder:
dir_path: Path,
languages: List[str] = None,
*,
source_root: Path,
project_id: int,
global_index_db_path: Path,
) -> DirBuildResult:
@@ -663,7 +702,7 @@ class IndexTreeBuilder:
store.initialize()
# Get source files in this directory only
source_files = self._iter_source_files(dir_path, languages)
source_files = self._iter_source_files(dir_path, languages, source_root=source_root)
files_count = 0
symbols_count = 0
@@ -731,7 +770,7 @@ class IndexTreeBuilder:
d.name
for d in dir_path.iterdir()
if d.is_dir()
and not self._is_ignored_dir(d)
and not self._is_ignored_dir(d, source_root=source_root)
]
store.update_merkle_root()
@@ -826,7 +865,7 @@ class IndexTreeBuilder:
)
def _iter_source_files(
self, dir_path: Path, languages: List[str] = None
self, dir_path: Path, languages: List[str] = None, source_root: Optional[Path] = None
) -> List[Path]:
"""Iterate source files in directory (non-recursive).
@@ -852,6 +891,9 @@ class IndexTreeBuilder:
if item.name.startswith("."):
continue
if self._is_filtered_file(item, source_root=source_root):
continue
# Check language support
language_id = self.config.language_for_path(item)
if not language_id:
@@ -1027,19 +1069,37 @@ def _compute_graph_neighbors(
# === Worker Function for ProcessPoolExecutor ===
def _matches_ignore_patterns(path: Path, patterns: List[str]) -> bool:
name = path.name
if name.startswith('.'):
return True
def _matches_path_patterns(path: Path, patterns: List[str], source_root: Optional[Path] = None) -> bool:
rel_path: Optional[str] = None
if source_root is not None:
try:
rel_path = path.relative_to(source_root).as_posix()
except ValueError:
rel_path = None
for pattern in patterns:
normalized = str(pattern).strip().replace('\\', '/').rstrip('/')
if not normalized:
continue
if normalized == name or fnmatch.fnmatch(name, normalized):
if normalized == path.name or fnmatch.fnmatch(path.name, normalized):
return True
if rel_path and (normalized == rel_path or fnmatch.fnmatch(rel_path, normalized)):
return True
return False
def _matches_ignore_patterns(path: Path, patterns: List[str], source_root: Optional[Path] = None) -> bool:
if path.name.startswith('.'):
return True
return _matches_path_patterns(path, patterns, source_root)
def _matches_extension_filters(path: Path, patterns: List[str], source_root: Optional[Path] = None) -> bool:
if not patterns:
return False
return _matches_path_patterns(path, patterns, source_root)
def _build_dir_worker(args: tuple) -> DirBuildResult:
"""Worker function for parallel directory building.
@@ -1047,12 +1107,12 @@ def _build_dir_worker(args: tuple) -> DirBuildResult:
Reconstructs necessary objects from serializable arguments.
Args:
args: Tuple of (dir_path, index_db_path, languages, config_dict, project_id, global_index_db_path)
args: Tuple of (dir_path, index_db_path, languages, config_dict, project_id, global_index_db_path, source_root)
Returns:
DirBuildResult for the directory
"""
dir_path, index_db_path, languages, config_dict, project_id, global_index_db_path = args
dir_path, index_db_path, languages, config_dict, project_id, global_index_db_path, source_root = args
# Reconstruct config
config = Config(
@@ -1064,9 +1124,11 @@ def _build_dir_worker(args: tuple) -> DirBuildResult:
static_graph_relationship_types=list(config_dict.get("static_graph_relationship_types", ["imports", "inherits"])),
use_astgrep=bool(config_dict.get("use_astgrep", False)),
ignore_patterns=list(config_dict.get("ignore_patterns", [])),
extension_filters=list(config_dict.get("extension_filters", [])),
)
parser_factory = ParserFactory(config)
source_root_path = Path(source_root) if source_root else None
global_index: GlobalSymbolIndex | None = None
try:
@@ -1092,6 +1154,9 @@ def _build_dir_worker(args: tuple) -> DirBuildResult:
if item.name.startswith("."):
continue
if _matches_extension_filters(item, config.extension_filters, source_root_path):
continue
language_id = config.language_for_path(item)
if not language_id:
continue
@@ -1146,7 +1211,7 @@ def _build_dir_worker(args: tuple) -> DirBuildResult:
subdirs = [
d.name
for d in dir_path.iterdir()
if d.is_dir() and not _matches_ignore_patterns(d, ignore_patterns)
if d.is_dir() and not _matches_ignore_patterns(d, ignore_patterns, source_root_path)
]
store.update_merkle_root()

View File

@@ -19,6 +19,7 @@ def test_load_settings_reads_ignore_patterns_and_extension_filters(tmp_path: Pat
)
config = Config(data_dir=tmp_path)
config.load_settings()
assert config.ignore_patterns == ["frontend/dist", "coverage"]
assert config.extension_filters == ["*.min.js", "*.map"]

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import json
from pathlib import Path
from unittest.mock import MagicMock
@@ -84,3 +85,63 @@ def test_collect_dirs_by_depth_respects_relative_ignore_patterns_from_config(tmp
assert "frontend/src" in discovered_dirs
assert "frontend/dist" not in discovered_dirs
def test_iter_source_files_respects_extension_filters_and_relative_patterns(tmp_path: Path) -> None:
frontend_dir = tmp_path / "frontend"
frontend_dir.mkdir()
(frontend_dir / "app.ts").write_text("export const app = 1\n", encoding="utf-8")
(frontend_dir / "bundle.min.js").write_text("export const bundle = 1\n", encoding="utf-8")
(frontend_dir / "skip.ts").write_text("export const skip = 1\n", encoding="utf-8")
builder = IndexTreeBuilder(
registry=MagicMock(),
mapper=MagicMock(),
config=Config(
data_dir=tmp_path / "data",
extension_filters=["*.min.js", "frontend/skip.ts"],
),
incremental=False,
)
source_files = builder._iter_source_files(frontend_dir, source_root=tmp_path)
assert [path.name for path in source_files] == ["app.ts"]
assert builder._should_index_dir(frontend_dir, source_root=tmp_path) is True
def test_builder_loads_saved_ignore_and_extension_filters_by_default(tmp_path: Path, monkeypatch) -> None:
codexlens_home = tmp_path / "codexlens-home"
codexlens_home.mkdir()
(codexlens_home / "settings.json").write_text(
json.dumps(
{
"ignore_patterns": ["frontend/dist"],
"extension_filters": ["*.min.js"],
}
),
encoding="utf-8",
)
monkeypatch.setenv("CODEXLENS_DATA_DIR", str(codexlens_home))
frontend_dir = tmp_path / "frontend"
frontend_dir.mkdir()
dist_dir = frontend_dir / "dist"
dist_dir.mkdir()
(frontend_dir / "app.ts").write_text("export const app = 1\n", encoding="utf-8")
(frontend_dir / "bundle.min.js").write_text("export const bundle = 1\n", encoding="utf-8")
(dist_dir / "compiled.ts").write_text("export const compiled = 1\n", encoding="utf-8")
builder = IndexTreeBuilder(
registry=MagicMock(),
mapper=MagicMock(),
config=None,
incremental=False,
)
source_files = builder._iter_source_files(frontend_dir, source_root=tmp_path)
dirs_by_depth = builder._collect_dirs_by_depth(tmp_path)
discovered_dirs = _relative_dirs(tmp_path, dirs_by_depth)
assert [path.name for path in source_files] == ["app.ts"]
assert "frontend/dist" not in discovered_dirs