mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-11 17:21:03 +08:00
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:
@@ -0,0 +1 @@
|
||||
{"ignore_patterns": ["frontend/dist"], "extension_filters": ["*.min.js"]}
|
||||
@@ -0,0 +1 @@
|
||||
export const app = 1
|
||||
1
codex-lens/.pytest-temp/test_builder_loads_saved_ignor0/frontend/bundle.min.js
vendored
Normal file
1
codex-lens/.pytest-temp/test_builder_loads_saved_ignor0/frontend/bundle.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export const bundle = 1
|
||||
1
codex-lens/.pytest-temp/test_builder_loads_saved_ignor0/frontend/dist/compiled.ts
vendored
Normal file
1
codex-lens/.pytest-temp/test_builder_loads_saved_ignor0/frontend/dist/compiled.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export const compiled = 1
|
||||
1
codex-lens/.pytest-temp/test_collect_dirs_by_depth_res0/frontend/dist/bundle.ts
vendored
Normal file
1
codex-lens/.pytest-temp/test_collect_dirs_by_depth_res0/frontend/dist/bundle.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export const bundle = 1
|
||||
@@ -0,0 +1 @@
|
||||
export const app = 1
|
||||
@@ -0,0 +1 @@
|
||||
print('artifact')
|
||||
@@ -0,0 +1 @@
|
||||
print('artifact')
|
||||
@@ -0,0 +1 @@
|
||||
print('artifact')
|
||||
@@ -0,0 +1 @@
|
||||
print('artifact')
|
||||
@@ -0,0 +1 @@
|
||||
print('artifact')
|
||||
1
codex-lens/.pytest-temp/test_collect_dirs_by_depth_ski0/dist/generated.py
vendored
Normal file
1
codex-lens/.pytest-temp/test_collect_dirs_by_depth_ski0/dist/generated.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
print('artifact')
|
||||
@@ -0,0 +1 @@
|
||||
print('artifact')
|
||||
@@ -0,0 +1 @@
|
||||
print('ok')
|
||||
@@ -0,0 +1 @@
|
||||
print('artifact')
|
||||
@@ -0,0 +1 @@
|
||||
export const app = 1
|
||||
1
codex-lens/.pytest-temp/test_iter_source_files_respect0/frontend/bundle.min.js
vendored
Normal file
1
codex-lens/.pytest-temp/test_iter_source_files_respect0/frontend/bundle.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export const bundle = 1
|
||||
@@ -0,0 +1 @@
|
||||
export const skip = 1
|
||||
@@ -0,0 +1 @@
|
||||
{"ignore_patterns": ["frontend/dist", "coverage"], "extension_filters": ["*.min.js", "*.map"]}
|
||||
1
codex-lens/.pytest-temp/test_should_index_dir_ignores_0/package/dist/bundle.py
vendored
Normal file
1
codex-lens/.pytest-temp/test_should_index_dir_ignores_0/package/dist/bundle.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
print('compiled')
|
||||
@@ -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()
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user