srcodex 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- srcodex/__init__.py +0 -0
- srcodex/backend/__init__.py +0 -0
- srcodex/backend/chat.py +79 -0
- srcodex/backend/main.py +98 -0
- srcodex/backend/services/__init__.py +0 -0
- srcodex/backend/services/claude_service.py +754 -0
- srcodex/backend/services/config_loader.py +113 -0
- srcodex/backend/services/file_access_tools.py +279 -0
- srcodex/backend/services/file_tree.py +480 -0
- srcodex/backend/services/graph_tools.py +874 -0
- srcodex/backend/services/logger_setup.py +91 -0
- srcodex/backend/services/session_manager.py +81 -0
- srcodex/backend/services/status_tracker.py +91 -0
- srcodex/cli.py +255 -0
- srcodex/core/__init__.py +0 -0
- srcodex/core/config.py +113 -0
- srcodex/core/logger.py +23 -0
- srcodex/indexer/__init__.py +0 -0
- srcodex/indexer/cscope_client.py +183 -0
- srcodex/indexer/ctags_compat.py +223 -0
- srcodex/indexer/ctags_parser.py +456 -0
- srcodex/indexer/explorer.py +135 -0
- srcodex/indexer/field_access_analyzer.py +436 -0
- srcodex/indexer/indexer.py +664 -0
- srcodex/indexer/reference_ingestor.py +293 -0
- srcodex/indexer/reference_resolver.py +544 -0
- srcodex/tui/__init__.py +0 -0
- srcodex/tui/app.py +103 -0
- srcodex/tui/app.tcss +24 -0
- srcodex/tui/components/__init__.py +0 -0
- srcodex/tui/components/bars/__init__.py +0 -0
- srcodex/tui/components/bars/chat_header.py +48 -0
- srcodex/tui/components/bars/code_tab_bar.py +157 -0
- srcodex/tui/components/bars/footer_bar.py +128 -0
- srcodex/tui/components/bars/left_tab.py +54 -0
- srcodex/tui/components/logger.py +57 -0
- srcodex/tui/components/panels/__init__.py +0 -0
- srcodex/tui/components/panels/chat_panel.py +523 -0
- srcodex/tui/components/panels/code_panel.py +229 -0
- srcodex/tui/components/panels/side_panel.py +128 -0
- srcodex/tui/components/views/__init__.py +0 -0
- srcodex/tui/components/views/explorer_view.py +20 -0
- srcodex/tui/components/views/search_view.py +148 -0
- srcodex/tui/components/widgets/__init__.py +0 -0
- srcodex/tui/components/widgets/file_browser.py +16 -0
- srcodex/tui/components/widgets/find_box.py +85 -0
- srcodex-0.2.0.dist-info/METADATA +170 -0
- srcodex-0.2.0.dist-info/RECORD +52 -0
- srcodex-0.2.0.dist-info/WHEEL +5 -0
- srcodex-0.2.0.dist-info/entry_points.txt +2 -0
- srcodex-0.2.0.dist-info/licenses/LICENSE +21 -0
- srcodex-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Cscope Client - Query cscope database for cross-references
|
|
4
|
+
This module provides a Python interface to query cscope databases.
|
|
5
|
+
It runs cscope commands and parses the output into structured data.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List, Dict, Optional
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Reference:
|
|
16
|
+
"""A single cross-reference result from cscope"""
|
|
17
|
+
file_path: str # File where the reference occurs
|
|
18
|
+
function: str # Function name containing the reference
|
|
19
|
+
line_number: int # Line number in the file
|
|
20
|
+
line_text: str # The actual line of code
|
|
21
|
+
|
|
22
|
+
class CscopeClient:
|
|
23
|
+
"""Client for querying cscope database"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, cscope_dir: str):
|
|
26
|
+
"""
|
|
27
|
+
Initialize cscope client
|
|
28
|
+
Args:
|
|
29
|
+
cscope_dir: Directory containing cscope.out and related files
|
|
30
|
+
"""
|
|
31
|
+
self.cscope_dir = Path(cscope_dir)
|
|
32
|
+
self.cscope_out = self.cscope_dir / "cscope.out"
|
|
33
|
+
|
|
34
|
+
if not self.cscope_out.exists():
|
|
35
|
+
raise FileNotFoundError(
|
|
36
|
+
f"Cscope database not found: {self.cscope_out}\n"
|
|
37
|
+
f"Run indexer with --cscope flag to build it."
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def _run_query(self, query_type: int, symbol: str) -> List[Reference]:
|
|
41
|
+
"""
|
|
42
|
+
Run a cscope query and parse results
|
|
43
|
+
Args:
|
|
44
|
+
query_type: Cscope query type (0-8)
|
|
45
|
+
symbol: Symbol to search for
|
|
46
|
+
Returns:
|
|
47
|
+
List of Reference objects
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
result = subprocess.run(
|
|
51
|
+
['cscope', '-dL', f'-{query_type}', symbol],
|
|
52
|
+
cwd=self.cscope_dir,
|
|
53
|
+
capture_output=True,
|
|
54
|
+
text=True,
|
|
55
|
+
check=True
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return self._parse_output(result.stdout)
|
|
59
|
+
|
|
60
|
+
except subprocess.CalledProcessError as e:
|
|
61
|
+
# Cscope returns non-zero if no results found
|
|
62
|
+
if e.returncode == 1 and not e.stderr:
|
|
63
|
+
return []
|
|
64
|
+
raise RuntimeError(f"Cscope query failed: {e.stderr}")
|
|
65
|
+
|
|
66
|
+
except FileNotFoundError:
|
|
67
|
+
raise RuntimeError(
|
|
68
|
+
"cscope command not found. Install with: sudo apt install cscope"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def _parse_output(self, output: str) -> List[Reference]:
|
|
72
|
+
"""
|
|
73
|
+
Parse cscope output into Reference objects
|
|
74
|
+
Cscope output format:
|
|
75
|
+
filename function_name line_number line_text
|
|
76
|
+
"""
|
|
77
|
+
references = []
|
|
78
|
+
|
|
79
|
+
for line in output.strip().split('\n'):
|
|
80
|
+
if not line:
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
# Split on whitespace (max 3 splits to preserve line_text)
|
|
84
|
+
parts = line.split(None, 3)
|
|
85
|
+
|
|
86
|
+
if len(parts) < 3:
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
file_path = parts[0]
|
|
90
|
+
function = parts[1]
|
|
91
|
+
line_number = int(parts[2])
|
|
92
|
+
line_text = parts[3] if len(parts) > 3 else ''
|
|
93
|
+
|
|
94
|
+
references.append(Reference(
|
|
95
|
+
file_path=file_path,
|
|
96
|
+
function=function,
|
|
97
|
+
line_number=line_number,
|
|
98
|
+
line_text=line_text
|
|
99
|
+
))
|
|
100
|
+
|
|
101
|
+
return references
|
|
102
|
+
|
|
103
|
+
"""
|
|
104
|
+
Below are API's to find references to symbols
|
|
105
|
+
Args: Symbol name, Function name, Text to search for
|
|
106
|
+
Returns: Lists of symbol references, functions...
|
|
107
|
+
"""
|
|
108
|
+
def find_symbol(self, symbol: str) -> List[Reference]:
|
|
109
|
+
# List of references where this symbol appears
|
|
110
|
+
return self._run_query(0, symbol)
|
|
111
|
+
|
|
112
|
+
def find_definition(self, symbol: str) -> List[Reference]:
|
|
113
|
+
# List with one reference (the definition location)
|
|
114
|
+
return self._run_query(1, symbol)
|
|
115
|
+
|
|
116
|
+
def find_callees(self, function: str) -> List[Reference]:
|
|
117
|
+
# List of functions called by this function
|
|
118
|
+
return self._run_query(2, function)
|
|
119
|
+
|
|
120
|
+
def find_callers(self, function: str) -> List[Reference]:
|
|
121
|
+
# List of functions that call this function
|
|
122
|
+
return self._run_query(3, function)
|
|
123
|
+
|
|
124
|
+
def find_text(self, text: str) -> List[Reference]:
|
|
125
|
+
# List of references where this text appears
|
|
126
|
+
return self._run_query(4, text)
|
|
127
|
+
|
|
128
|
+
def find_egrep_pattern(self, pattern: str) -> List[Reference]:
|
|
129
|
+
# List of references matching the pattern
|
|
130
|
+
return self._run_query(6, pattern)
|
|
131
|
+
|
|
132
|
+
def find_files_including(self, filename: str) -> List[Reference]:
|
|
133
|
+
# List of files that include this file
|
|
134
|
+
return self._run_query(8, filename)
|
|
135
|
+
|
|
136
|
+
def get_stats(self) -> Dict[str, any]:
|
|
137
|
+
# Dictionary with database information
|
|
138
|
+
stats = {
|
|
139
|
+
'database_path': str(self.cscope_out),
|
|
140
|
+
'database_exists': self.cscope_out.exists(),
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if self.cscope_out.exists():
|
|
144
|
+
stats['database_size_mb'] = self.cscope_out.stat().st_size / (1024 * 1024)
|
|
145
|
+
|
|
146
|
+
# Check for cscope.files
|
|
147
|
+
cscope_files = self.cscope_dir / "cscope.files"
|
|
148
|
+
if cscope_files.exists():
|
|
149
|
+
with open(cscope_files) as f:
|
|
150
|
+
stats['indexed_files'] = len(f.readlines())
|
|
151
|
+
|
|
152
|
+
return stats
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# Convenience function for quick queries
|
|
156
|
+
def query_cscope(cscope_dir: str, query_type: str, symbol: str) -> List[Reference]:
|
|
157
|
+
"""
|
|
158
|
+
Convenience function for one-off cscope queries
|
|
159
|
+
Args:
|
|
160
|
+
cscope_dir: Directory containing cscope.out
|
|
161
|
+
query_type: Query type (symbol, definition, callers, callees, text, includes)
|
|
162
|
+
symbol: Symbol/text to search for
|
|
163
|
+
Returns:
|
|
164
|
+
List of references
|
|
165
|
+
"""
|
|
166
|
+
client = CscopeClient(cscope_dir)
|
|
167
|
+
|
|
168
|
+
query_map = {
|
|
169
|
+
'symbol': client.find_symbol,
|
|
170
|
+
'definition': client.find_definition,
|
|
171
|
+
'callers': client.find_callers,
|
|
172
|
+
'callees': client.find_callees,
|
|
173
|
+
'text': client.find_text,
|
|
174
|
+
'includes': client.find_files_including,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if query_type not in query_map:
|
|
178
|
+
raise ValueError(
|
|
179
|
+
f"Invalid query type: {query_type}\n"
|
|
180
|
+
f"Valid types: {', '.join(query_map.keys())}"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return query_map[query_type](symbol)
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
SRC Code Explorer - CTags Compatibility Check
|
|
4
|
+
Verifies that Universal CTags outputs expected kind values.
|
|
5
|
+
This prevents silent failures when different ctags versions/builds use different kind names.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import subprocess
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import tempfile
|
|
12
|
+
from typing import Set
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Expected ctags kind values that type_map in ctags_parser.py knows about
|
|
16
|
+
EXPECTED_KINDS = {
|
|
17
|
+
'function',
|
|
18
|
+
'prototype',
|
|
19
|
+
'variable',
|
|
20
|
+
'struct',
|
|
21
|
+
'union',
|
|
22
|
+
'enum',
|
|
23
|
+
'enumerator',
|
|
24
|
+
'typedef',
|
|
25
|
+
'macro',
|
|
26
|
+
'member',
|
|
27
|
+
'header',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Core kinds that MUST appear in test code
|
|
31
|
+
CORE_KINDS = {
|
|
32
|
+
'function',
|
|
33
|
+
'prototype',
|
|
34
|
+
'macro',
|
|
35
|
+
'typedef',
|
|
36
|
+
'member',
|
|
37
|
+
'variable',
|
|
38
|
+
'enumerator',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def verify_ctags_compatibility(ctags_bin: str = "ctags") -> None:
|
|
43
|
+
"""
|
|
44
|
+
This startup check ensures that:
|
|
45
|
+
1. ctags is installed and working
|
|
46
|
+
2. Kind values match what our type_map expects
|
|
47
|
+
3. We fail early with a clear error vs silent data corruption
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
version_result = subprocess.run(
|
|
52
|
+
[ctags_bin, "--version"],
|
|
53
|
+
capture_output=True,
|
|
54
|
+
check=True,
|
|
55
|
+
text=True
|
|
56
|
+
)
|
|
57
|
+
version_info = version_result.stdout.strip().split('\n')[0]
|
|
58
|
+
except (FileNotFoundError, subprocess.CalledProcessError) as e:
|
|
59
|
+
raise RuntimeError(f"ctags not found or failed to run: {e}")
|
|
60
|
+
|
|
61
|
+
# Minimal test C code with all expected symbol types
|
|
62
|
+
test_code = """
|
|
63
|
+
#define TEST_MACRO 1
|
|
64
|
+
typedef struct { int member_x; } test_struct_t;
|
|
65
|
+
typedef union { int u_val; } test_union_t;
|
|
66
|
+
typedef enum { ENUM_VAL = 0 } test_enum_t;
|
|
67
|
+
void test_func(void);
|
|
68
|
+
void test_func(void) {}
|
|
69
|
+
static int test_static_var = 0;
|
|
70
|
+
int test_global_var;
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
# Write test code to temporary file
|
|
74
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.c', delete=False) as f:
|
|
75
|
+
test_file = f.name
|
|
76
|
+
f.write(test_code)
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
# Run ctags with EXACT PRODUCTION FLAGS
|
|
80
|
+
# This verifies both ctags availability AND flag compatibility
|
|
81
|
+
cmd = [
|
|
82
|
+
ctags_bin,
|
|
83
|
+
"--output-format=json",
|
|
84
|
+
"--fields=+nKSz",
|
|
85
|
+
"--kinds-C=+p",
|
|
86
|
+
"-f", "-",
|
|
87
|
+
test_file
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
92
|
+
except subprocess.CalledProcessError as e:
|
|
93
|
+
# Command failed - likely bad flags
|
|
94
|
+
raise RuntimeError(
|
|
95
|
+
f"\n"
|
|
96
|
+
f"╔══════════════════════════════════════════════════════════════════╗\n"
|
|
97
|
+
f"║ CTAGS COMMAND FAILED ║\n"
|
|
98
|
+
f"╚══════════════════════════════════════════════════════════════════╝\n"
|
|
99
|
+
f"\n"
|
|
100
|
+
f"ctags version: {version_info}\n"
|
|
101
|
+
f"\n"
|
|
102
|
+
f"Command that failed:\n"
|
|
103
|
+
f" {' '.join(cmd)}\n"
|
|
104
|
+
f"\n"
|
|
105
|
+
f"Error output:\n"
|
|
106
|
+
f" {e.stderr}\n"
|
|
107
|
+
f"\n"
|
|
108
|
+
f"This likely means ctags doesn't support the flags we use.\n"
|
|
109
|
+
f"Common issues:\n"
|
|
110
|
+
f" - Old ctags: --kinds-C syntax not supported (try --c-kinds)\n"
|
|
111
|
+
f" - Exuberant ctags: Missing --output-format=json support\n"
|
|
112
|
+
f" - BSD ctags: Not compatible (needs Universal CTags)\n"
|
|
113
|
+
f"\n"
|
|
114
|
+
f"Please install Universal CTags:\n"
|
|
115
|
+
f" Ubuntu/Debian: sudo apt install universal-ctags\n"
|
|
116
|
+
f" macOS: brew install universal-ctags\n"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Extract unique kind values from ctags output
|
|
120
|
+
# ALSO verify 'path' field is present (critical for parse_root)
|
|
121
|
+
observed_kinds = set()
|
|
122
|
+
has_path_field = False
|
|
123
|
+
for line in result.stdout.strip().split('\n'):
|
|
124
|
+
if not line or line.startswith('!'):
|
|
125
|
+
continue
|
|
126
|
+
try:
|
|
127
|
+
tag = json.loads(line)
|
|
128
|
+
kind = tag.get('kind')
|
|
129
|
+
if kind:
|
|
130
|
+
observed_kinds.add(kind)
|
|
131
|
+
|
|
132
|
+
# Check for 'path' field (required for grouping symbols by file)
|
|
133
|
+
if 'path' in tag:
|
|
134
|
+
has_path_field = True
|
|
135
|
+
except json.JSONDecodeError:
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
# Check 0: Verify 'path' field is present in output
|
|
139
|
+
if not has_path_field:
|
|
140
|
+
raise RuntimeError(
|
|
141
|
+
f"\n"
|
|
142
|
+
f"╔══════════════════════════════════════════════════════════════════╗\n"
|
|
143
|
+
f"║ CTAGS COMPATIBILITY CHECK FAILED ║\n"
|
|
144
|
+
f"╚══════════════════════════════════════════════════════════════════╝\n"
|
|
145
|
+
f"\n"
|
|
146
|
+
f"ctags version: {version_info}\n"
|
|
147
|
+
f"\n"
|
|
148
|
+
f"CRITICAL: 'path' field not found in ctags JSON output!\n"
|
|
149
|
+
f"\n"
|
|
150
|
+
f"The 'path' field is required to group symbols by file.\n"
|
|
151
|
+
f"Without it, parse_root() cannot function.\n"
|
|
152
|
+
f"\n"
|
|
153
|
+
f"Command that produced output:\n"
|
|
154
|
+
f" {' '.join(cmd)}\n"
|
|
155
|
+
f"\n"
|
|
156
|
+
f"Please verify:\n"
|
|
157
|
+
f" - Using Universal CTags (not Exuberant)\n"
|
|
158
|
+
f" - JSON output format enabled\n"
|
|
159
|
+
f" - All required fields present\n"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Check 1: Did we see the core kinds we expect?
|
|
163
|
+
missing_core = CORE_KINDS - observed_kinds
|
|
164
|
+
if missing_core:
|
|
165
|
+
raise RuntimeError(
|
|
166
|
+
f"\n"
|
|
167
|
+
f"╔══════════════════════════════════════════════════════════════════╗\n"
|
|
168
|
+
f"║ CTAGS COMPATIBILITY CHECK FAILED ║\n"
|
|
169
|
+
f"╚══════════════════════════════════════════════════════════════════╝\n"
|
|
170
|
+
f"\n"
|
|
171
|
+
f"ctags version: {version_info}\n"
|
|
172
|
+
f"\n"
|
|
173
|
+
f"Missing expected core kinds: {missing_core}\n"
|
|
174
|
+
f"Expected kinds: {EXPECTED_KINDS}\n"
|
|
175
|
+
f"Observed kinds: {observed_kinds}\n"
|
|
176
|
+
f"\n"
|
|
177
|
+
f"Your ctags installation may be incompatible.\n"
|
|
178
|
+
f"Please install Universal CTags:\n"
|
|
179
|
+
f" Ubuntu/Debian: sudo apt install universal-ctags\n"
|
|
180
|
+
f" macOS: brew install universal-ctags\n"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Check 2: Did we see unexpected kinds not in our type_map?
|
|
184
|
+
unexpected_kinds = observed_kinds - EXPECTED_KINDS
|
|
185
|
+
if unexpected_kinds:
|
|
186
|
+
print(f" Warning: ctags returned unexpected kinds: {unexpected_kinds}")
|
|
187
|
+
print(f" These will be stored as-is in the database.")
|
|
188
|
+
print(f" ctags version: {version_info}")
|
|
189
|
+
print()
|
|
190
|
+
|
|
191
|
+
finally:
|
|
192
|
+
try:
|
|
193
|
+
os.unlink(test_file)
|
|
194
|
+
except OSError:
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def get_ctags_version(ctags_bin: str = "ctags") -> str:
|
|
199
|
+
"""
|
|
200
|
+
Get ctags version string for debugging/logging.
|
|
201
|
+
"""
|
|
202
|
+
try:
|
|
203
|
+
result = subprocess.run(
|
|
204
|
+
[ctags_bin, "--version"],
|
|
205
|
+
capture_output=True,
|
|
206
|
+
check=True,
|
|
207
|
+
text=True
|
|
208
|
+
)
|
|
209
|
+
return result.stdout.strip().split('\n')[0]
|
|
210
|
+
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
211
|
+
return "unknown (ctags not found)"
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# Simple test
|
|
215
|
+
if __name__ == "__main__":
|
|
216
|
+
print("Running ctags compatibility check...")
|
|
217
|
+
try:
|
|
218
|
+
verify_ctags_compatibility()
|
|
219
|
+
print("ctags compatibility check PASSED")
|
|
220
|
+
print(f"Version: {get_ctags_version()}")
|
|
221
|
+
except RuntimeError as e:
|
|
222
|
+
print(f"{e}")
|
|
223
|
+
exit(1)
|