Add regex filters to clangd navigation
This commit is contained in:
13
AGENTS.md
13
AGENTS.md
@@ -69,16 +69,23 @@ flat source tree and extracted components:
|
||||
```powershell
|
||||
python scripts/dev/clangd_nav.py symbols --file src/app_core/brush_ui.h --name execute_brush
|
||||
python scripts/dev/clangd_nav.py symbols --file src/app_core/brush_ui.h --name-regex "execute_.*preset"
|
||||
python scripts/dev/clangd_nav.py symbols --file src/app_core/document_export.h --detail-regex "Export.*Plan"
|
||||
python scripts/dev/clangd_nav.py definition --file src/node_panel_brush.cpp --line 511 --column 39
|
||||
python scripts/dev/clangd_nav.py references --file src/app_core/brush_ui.h --line 783 --column 45
|
||||
python scripts/dev/clangd_nav.py references --file src/app_core/brush_ui.h --line 783 --column 45 --path-regex "src[\\/]app_core"
|
||||
python scripts/dev/clangd_nav.py self-test
|
||||
```
|
||||
|
||||
The helper talks to `clangd` using an existing `compile_commands.json`. It
|
||||
defaults to `out/build/windows-clangcl-asan` and then `out/build/android-arm64`;
|
||||
pass `--compile-commands-dir` or set `PP_CLANGD_COMPILE_COMMANDS_DIR` when using
|
||||
another Ninja build tree. Use `--name` and `--max-results` to keep output small.
|
||||
Use `--name-regex` for regex filtering against `qualifiedName`; matching is
|
||||
case-insensitive by default, and `--no-ignore-case` makes it case-sensitive.
|
||||
Use `--name-regex` for regex filtering against `qualifiedName`,
|
||||
`--detail-regex` for symbol detail/signature filtering, and `--path-regex` for
|
||||
definition/declaration/implementation/reference location filtering. Regex
|
||||
matching is case-insensitive by default, and `--no-ignore-case` makes it
|
||||
case-sensitive. Run `python scripts/dev/clangd_nav.py self-test` or the
|
||||
`panopainter_clangd_nav_regex_self_test` CTest before relying on regex behavior
|
||||
after tool changes.
|
||||
Treat symbol, hover, declaration, definition, and implementation lookups as the
|
||||
reliable path. Reference lookups are riskier because a one-shot clangd process
|
||||
may not have a complete project index; the helper refuses reference queries
|
||||
|
||||
@@ -77,7 +77,10 @@ powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -P
|
||||
cmake --fresh --preset windows-clangcl-asan
|
||||
python scripts/dev/clangd_nav.py symbols --file src/app_core/brush_ui.h --name execute_brush
|
||||
python scripts/dev/clangd_nav.py symbols --file src/app_core/brush_ui.h --name-regex "execute_.*preset"
|
||||
python scripts/dev/clangd_nav.py symbols --file src/app_core/document_export.h --detail-regex "Export.*Plan"
|
||||
python scripts/dev/clangd_nav.py definition --file src/node_panel_brush.cpp --line 511 --column 39
|
||||
python scripts/dev/clangd_nav.py references --file src/app_core/brush_ui.h --line 783 --column 45 --path-regex "src[\\/]app_core"
|
||||
python scripts/dev/clangd_nav.py self-test
|
||||
```
|
||||
|
||||
Known local toolchain state:
|
||||
@@ -97,9 +100,15 @@ Known local toolchain state:
|
||||
the current `compile_commands.json` from `windows-clangcl-asan`,
|
||||
`android-arm64`, or a caller-provided build directory. Symbol, hover,
|
||||
declaration, definition, and implementation lookups are the reliable use case.
|
||||
Symbol listing supports substring filtering with `--name` and regex filtering
|
||||
against `qualifiedName` with `--name-regex`; regex matching is case-insensitive
|
||||
by default and can be made case-sensitive with `--no-ignore-case`.
|
||||
Symbol listing supports substring filtering with `--name`, regex filtering
|
||||
against `qualifiedName` with `--name-regex`, and symbol detail/signature
|
||||
regex filtering with `--detail-regex`. Definition, declaration,
|
||||
implementation, and reference location output supports `--path-regex` for
|
||||
path/URI filtering. Regex matching is case-insensitive by default and can be
|
||||
made case-sensitive with `--no-ignore-case`. The helper exposes
|
||||
`python scripts/dev/clangd_nav.py self-test`, also registered as
|
||||
`panopainter_clangd_nav_regex_self_test`, so regex filter behavior can be
|
||||
validated without clangd or a compile database.
|
||||
Reference lookup is guarded because a one-shot clangd process may not have a
|
||||
complete project index: pass `--background-index` for broader best-effort
|
||||
results or `--allow-incomplete-references` for explicitly
|
||||
|
||||
@@ -317,7 +317,10 @@ with JSON automation commands for app document-open routing, app session
|
||||
dirty-state and save decisions, creating a `pp_document` model, metadata-only
|
||||
PPI project loading, and inspecting image signatures, PPI headers, and layout
|
||||
XML; full document/app integration is debt-tracked as DEBT-0010 and full PPI
|
||||
body parsing is debt-tracked as DEBT-0013.
|
||||
body parsing is debt-tracked as DEBT-0013. Agent code navigation now includes
|
||||
`scripts/dev/clangd_nav.py` with symbol/detail/path regex filters and a
|
||||
`panopainter_clangd_nav_regex_self_test` CTest so broad symbol-family searches
|
||||
can be validated before they guide refactors.
|
||||
|
||||
Implementation tasks:
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
Examples:
|
||||
python scripts/dev/clangd_nav.py symbols --file src/app_core/brush_ui.h
|
||||
python scripts/dev/clangd_nav.py symbols --file src/app_core/brush_ui.h --name-regex "execute_.*preset"
|
||||
python scripts/dev/clangd_nav.py symbols --file src/app_core/document_export.h --detail-regex "Export.*Plan"
|
||||
python scripts/dev/clangd_nav.py definition --file src/node_panel_brush.cpp --line 410 --column 30
|
||||
python scripts/dev/clangd_nav.py references --file src/app_core/brush_ui.h --line 192 --column 43
|
||||
python scripts/dev/clangd_nav.py references --file src/app_core/brush_ui.h --line 192 --column 43 --path-regex "src[\\\\/]app_core"
|
||||
python scripts/dev/clangd_nav.py self-test
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -20,7 +22,7 @@ import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from typing import Any
|
||||
from typing import Any, Pattern
|
||||
|
||||
|
||||
DEFAULT_BUILD_DIRS = (
|
||||
@@ -301,6 +303,120 @@ def _hover_to_json(result: Any) -> dict[str, Any] | None:
|
||||
return output
|
||||
|
||||
|
||||
def _compile_optional_regex(pattern: str | None, option_name: str, ignore_case: bool) -> Pattern[str] | None:
|
||||
if not pattern:
|
||||
return None
|
||||
flags = re.IGNORECASE if ignore_case else 0
|
||||
try:
|
||||
return re.compile(pattern, flags)
|
||||
except re.error as exc:
|
||||
raise SystemExit(f"invalid {option_name}: {exc}") from exc
|
||||
|
||||
|
||||
def _regex_matches(regex: Pattern[str] | None, value: str) -> bool:
|
||||
return regex is None or regex.search(value) is not None
|
||||
|
||||
|
||||
def _filter_flat_symbols(
|
||||
symbols: list[dict[str, Any]],
|
||||
name_substring: str | None,
|
||||
name_regex: Pattern[str] | None,
|
||||
detail_regex: Pattern[str] | None,
|
||||
) -> list[dict[str, Any]]:
|
||||
filtered = symbols
|
||||
if name_substring:
|
||||
needle = name_substring.lower()
|
||||
filtered = [
|
||||
symbol for symbol in filtered
|
||||
if needle in symbol["qualifiedName"].lower()
|
||||
]
|
||||
if name_regex:
|
||||
filtered = [
|
||||
symbol for symbol in filtered
|
||||
if name_regex.search(symbol["qualifiedName"])
|
||||
]
|
||||
if detail_regex:
|
||||
filtered = [
|
||||
symbol for symbol in filtered
|
||||
if detail_regex.search(symbol.get("detail", ""))
|
||||
]
|
||||
return filtered
|
||||
|
||||
|
||||
def _filter_locations(
|
||||
locations: list[dict[str, Any]],
|
||||
path_regex: Pattern[str] | None,
|
||||
) -> list[dict[str, Any]]:
|
||||
if not path_regex:
|
||||
return locations
|
||||
return [
|
||||
location for location in locations
|
||||
if _regex_matches(path_regex, location.get("path", ""))
|
||||
or _regex_matches(path_regex, location.get("uri", ""))
|
||||
]
|
||||
|
||||
|
||||
def _run_self_test() -> int:
|
||||
name_regex = _compile_optional_regex(r"node(panel|dialog)::open_.*", "--name-regex", True)
|
||||
detail_regex = _compile_optional_regex(r"export.*plan", "--detail-regex", True)
|
||||
path_regex = _compile_optional_regex(r"src[\\/]app(_dialogs)?\.cpp$", "--path-regex", True)
|
||||
case_sensitive_regex = _compile_optional_regex(r"Brush", "--name-regex", False)
|
||||
|
||||
symbols = [
|
||||
{
|
||||
"qualifiedName": "NodePanel::open_project",
|
||||
"detail": "void()",
|
||||
},
|
||||
{
|
||||
"qualifiedName": "NodeDialog::open_export",
|
||||
"detail": "ExportTargetPlan()",
|
||||
},
|
||||
{
|
||||
"qualifiedName": "Brush::open_project",
|
||||
"detail": "BrushPlan()",
|
||||
},
|
||||
]
|
||||
name_matches = _filter_flat_symbols(symbols, None, name_regex, None)
|
||||
detail_matches = _filter_flat_symbols(symbols, None, None, detail_regex)
|
||||
case_sensitive_matches = _filter_flat_symbols(symbols, None, case_sensitive_regex, None)
|
||||
|
||||
locations = [
|
||||
{
|
||||
"path": r"D:\Dev\panopainter\src\app.cpp",
|
||||
"uri": "file:///D:/Dev/panopainter/src/app.cpp",
|
||||
},
|
||||
{
|
||||
"path": r"D:\Dev\panopainter\src\app_dialogs.cpp",
|
||||
"uri": "file:///D:/Dev/panopainter/src/app_dialogs.cpp",
|
||||
},
|
||||
{
|
||||
"path": r"D:\Dev\panopainter\docs\modernization\roadmap.md",
|
||||
"uri": "file:///D:/Dev/panopainter/docs/modernization/roadmap.md",
|
||||
},
|
||||
]
|
||||
path_matches = _filter_locations(locations, path_regex)
|
||||
|
||||
checks = {
|
||||
"nameRegexMatchesAlternationAndWildcard": len(name_matches) == 2,
|
||||
"detailRegexMatchesSymbolDetail": len(detail_matches) == 1
|
||||
and detail_matches[0]["qualifiedName"] == "NodeDialog::open_export",
|
||||
"caseSensitiveRegexHonorsNoIgnoreCase": len(case_sensitive_matches) == 1
|
||||
and case_sensitive_matches[0]["qualifiedName"] == "Brush::open_project",
|
||||
"pathRegexFiltersLocations": len(path_matches) == 2
|
||||
and all("src" in location["path"] for location in path_matches),
|
||||
}
|
||||
ok = all(checks.values())
|
||||
print(json.dumps(
|
||||
{
|
||||
"ok": ok,
|
||||
"command": "self-test",
|
||||
"checks": checks,
|
||||
},
|
||||
indent=2,
|
||||
))
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
def _open_document(client: ClangdClient, file_path: Path) -> None:
|
||||
language_id = "cpp"
|
||||
if file_path.suffix.lower() in { ".h", ".hpp", ".hh", ".hxx" }:
|
||||
@@ -322,16 +438,27 @@ def _open_document(client: ClangdClient, file_path: Path) -> None:
|
||||
|
||||
|
||||
def run(args: argparse.Namespace) -> int:
|
||||
if args.command == "self-test":
|
||||
return _run_self_test()
|
||||
|
||||
if not args.file:
|
||||
raise SystemExit("--file is required for clangd navigation commands")
|
||||
|
||||
symbol_filters = [args.name, args.name_regex, args.detail_regex]
|
||||
if any(symbol_filters) and args.command != "symbols":
|
||||
raise SystemExit("--name, --name-regex, and --detail-regex are only supported by the symbols command")
|
||||
if args.path_regex and args.command not in { "definition", "declaration", "implementation", "references" }:
|
||||
raise SystemExit("--path-regex is only supported by location commands")
|
||||
if args.hierarchical and any(symbol_filters):
|
||||
raise SystemExit("--name, --name-regex, and --detail-regex require flat symbols; omit --hierarchical")
|
||||
|
||||
repo_root = _repo_root()
|
||||
compile_commands_dir = _find_compile_commands_dir(repo_root, args.compile_commands_dir)
|
||||
file_path = _resolve_file(repo_root, args.file)
|
||||
|
||||
name_regex = None
|
||||
if args.name_regex:
|
||||
try:
|
||||
name_regex = re.compile(args.name_regex, re.IGNORECASE if args.ignore_case else 0)
|
||||
except re.error as exc:
|
||||
raise SystemExit(f"invalid --name-regex: {exc}") from exc
|
||||
name_regex = _compile_optional_regex(args.name_regex, "--name-regex", args.ignore_case)
|
||||
detail_regex = _compile_optional_regex(args.detail_regex, "--detail-regex", args.ignore_case)
|
||||
path_regex = _compile_optional_regex(args.path_regex, "--path-regex", args.ignore_case)
|
||||
|
||||
if args.command == "references" and not args.background_index and not args.allow_incomplete_references:
|
||||
raise SystemExit(
|
||||
@@ -375,17 +502,7 @@ def run(args: argparse.Namespace) -> int:
|
||||
result_count = len(symbols)
|
||||
else:
|
||||
flattened = _flatten_symbols(symbols)
|
||||
if args.name:
|
||||
needle = args.name.lower()
|
||||
flattened = [
|
||||
symbol for symbol in flattened
|
||||
if needle in symbol["qualifiedName"].lower()
|
||||
]
|
||||
if name_regex:
|
||||
flattened = [
|
||||
symbol for symbol in flattened
|
||||
if name_regex.search(symbol["qualifiedName"])
|
||||
]
|
||||
flattened = _filter_flat_symbols(flattened, args.name, name_regex, detail_regex)
|
||||
result_count = len(flattened)
|
||||
result, truncated = _limit_results(flattened, args.max_results)
|
||||
elif command == "hover":
|
||||
@@ -394,6 +511,7 @@ def run(args: argparse.Namespace) -> int:
|
||||
params = _position_params(file_path, args.line, args.column)
|
||||
params["context"] = { "includeDeclaration": args.include_declaration }
|
||||
locations = _locations_to_json(client.request("textDocument/references", params))
|
||||
locations = _filter_locations(locations, path_regex)
|
||||
result_count = len(locations)
|
||||
result, truncated = _limit_results(locations, args.max_results)
|
||||
else:
|
||||
@@ -403,6 +521,7 @@ def run(args: argparse.Namespace) -> int:
|
||||
"implementation": "textDocument/implementation",
|
||||
}[command]
|
||||
locations = _locations_to_json(client.request(method, _position_params(file_path, args.line, args.column)))
|
||||
locations = _filter_locations(locations, path_regex)
|
||||
result_count = len(locations)
|
||||
result, truncated = _limit_results(locations, args.max_results)
|
||||
|
||||
@@ -417,6 +536,13 @@ def run(args: argparse.Namespace) -> int:
|
||||
"not-applicable" if command != "references"
|
||||
else ("best-effort-background-index" if args.background_index else "current-translation-unit-only")
|
||||
),
|
||||
"filters": {
|
||||
"name": args.name,
|
||||
"nameRegex": args.name_regex,
|
||||
"detailRegex": args.detail_regex,
|
||||
"pathRegex": args.path_regex,
|
||||
"ignoreCase": args.ignore_case,
|
||||
},
|
||||
"resultCount": result_count,
|
||||
"truncated": truncated,
|
||||
"result": result,
|
||||
@@ -432,9 +558,9 @@ def main(argv: list[str]) -> int:
|
||||
parser = argparse.ArgumentParser(description="Navigate C++ symbols through clangd JSON-RPC.")
|
||||
parser.add_argument(
|
||||
"command",
|
||||
choices=("definition", "declaration", "implementation", "references", "hover", "symbols"),
|
||||
choices=("definition", "declaration", "implementation", "references", "hover", "symbols", "self-test"),
|
||||
)
|
||||
parser.add_argument("--file", required=True, help="Source/header file to open.")
|
||||
parser.add_argument("--file", help="Source/header file to open.")
|
||||
parser.add_argument("--line", type=int, default=1, help="1-based line for position commands.")
|
||||
parser.add_argument("--column", type=int, default=1, help="1-based column for position commands.")
|
||||
parser.add_argument(
|
||||
@@ -445,6 +571,8 @@ def main(argv: list[str]) -> int:
|
||||
parser.add_argument("--timeout", type=float, default=20.0, help="Request timeout in seconds.")
|
||||
parser.add_argument("--name", help="Case-insensitive symbol-name filter for symbols command.")
|
||||
parser.add_argument("--name-regex", help="Regex filter for symbols command, matched against qualifiedName.")
|
||||
parser.add_argument("--detail-regex", help="Regex filter for symbols command, matched against detail.")
|
||||
parser.add_argument("--path-regex", help="Regex filter for definition/declaration/implementation/references paths.")
|
||||
parser.add_argument(
|
||||
"--ignore-case",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
find_package(Python3 COMPONENTS Interpreter REQUIRED)
|
||||
|
||||
add_test(NAME panopainter_clangd_nav_regex_self_test
|
||||
COMMAND "${Python3_EXECUTABLE}" "${PROJECT_SOURCE_DIR}/scripts/dev/clangd_nav.py" self-test)
|
||||
set_tests_properties(panopainter_clangd_nav_regex_self_test PROPERTIES
|
||||
LABELS "tooling;desktop-fast")
|
||||
|
||||
add_library(pp_test_harness INTERFACE)
|
||||
target_include_directories(pp_test_harness INTERFACE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
|
||||
Reference in New Issue
Block a user