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,480 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File Tree Service - Left Panel Navigation
|
|
3
|
+
|
|
4
|
+
Provides hierarchical file tree structure from the database for UI rendering.
|
|
5
|
+
Queries the 'files' table to build directory/file trees.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sqlite3
|
|
9
|
+
from typing import Dict, List, Optional
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FileTreeService:
|
|
14
|
+
def __init__(self, db_path: str):
|
|
15
|
+
self.db_path = db_path
|
|
16
|
+
|
|
17
|
+
def _get_connection(self) -> sqlite3.Connection:
|
|
18
|
+
"""Get database connection"""
|
|
19
|
+
conn = sqlite3.connect(self.db_path)
|
|
20
|
+
conn.row_factory = sqlite3.Row
|
|
21
|
+
return conn
|
|
22
|
+
|
|
23
|
+
def get_root(self) -> Dict:
|
|
24
|
+
"""
|
|
25
|
+
Get metadata about the project's logical root directory.
|
|
26
|
+
|
|
27
|
+
Root = the directory that was indexed (anchor for all relative paths).
|
|
28
|
+
|
|
29
|
+
Returns metadata only
|
|
30
|
+
- Project ID (derived from db filename)
|
|
31
|
+
- Display name
|
|
32
|
+
- Physical path (where source was indexed from)
|
|
33
|
+
- Logical path (always "" for root)
|
|
34
|
+
- Statistics (total files, symbols, children count)
|
|
35
|
+
"""
|
|
36
|
+
with self._get_connection() as conn:
|
|
37
|
+
cursor = conn.cursor()
|
|
38
|
+
|
|
39
|
+
# Get project metadata from metadata table
|
|
40
|
+
cursor.execute("SELECT key, value FROM metadata")
|
|
41
|
+
metadata = {row['key']: row['value'] for row in cursor.fetchall()}
|
|
42
|
+
|
|
43
|
+
# Count immediate children (top-level directories/files)
|
|
44
|
+
cursor.execute("""
|
|
45
|
+
SELECT COUNT(*) as count FROM (
|
|
46
|
+
SELECT DISTINCT
|
|
47
|
+
CASE
|
|
48
|
+
WHEN instr(path, '/') > 0
|
|
49
|
+
THEN substr(path, 1, instr(path, '/') - 1)
|
|
50
|
+
ELSE path
|
|
51
|
+
END as top_level
|
|
52
|
+
FROM files
|
|
53
|
+
)
|
|
54
|
+
""")
|
|
55
|
+
children_count = cursor.fetchone()['count']
|
|
56
|
+
project_id = Path(self.db_path).stem
|
|
57
|
+
physical_path = metadata.get('source_root', '')
|
|
58
|
+
display_name = Path(physical_path).name.upper() if physical_path else project_id.upper()
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
"id": project_id,
|
|
62
|
+
"path": "", # Logical root is always empty string
|
|
63
|
+
"physical_path": physical_path,
|
|
64
|
+
"is_dir": True,
|
|
65
|
+
"children_count": children_count,
|
|
66
|
+
"total_files": int(metadata.get('total_files', 0)),
|
|
67
|
+
"total_symbols": int(metadata.get('total_symbols', 0)),
|
|
68
|
+
"indexed_at": metadata.get('indexed_at', '')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
def get_children(self, path: str = "") -> List[Dict]:
|
|
72
|
+
"""
|
|
73
|
+
Get immediate children of a directory.
|
|
74
|
+
|
|
75
|
+
Input contract:
|
|
76
|
+
- path == "" → root children
|
|
77
|
+
|
|
78
|
+
Output contract:
|
|
79
|
+
- Returns immediate children only (not recursive)
|
|
80
|
+
- Each child includes:
|
|
81
|
+
* name: basename
|
|
82
|
+
* path: full relative path (dirs end with /)
|
|
83
|
+
* kind: "file" or "dir"
|
|
84
|
+
* children_count: (dirs only) number of immediate children
|
|
85
|
+
* symbol_count: total symbols (files: direct, dirs: recursive)
|
|
86
|
+
* size: (files only) file size in bytes
|
|
87
|
+
|
|
88
|
+
Algorithm:
|
|
89
|
+
1. Get all files matching prefix
|
|
90
|
+
2. Strip prefix, take next segment up to /
|
|
91
|
+
3. If segment has / after it → dir, else → file
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
if path and not path.endswith('/'):
|
|
95
|
+
path = path + '/'
|
|
96
|
+
|
|
97
|
+
with self._get_connection() as conn:
|
|
98
|
+
cursor = conn.cursor()
|
|
99
|
+
|
|
100
|
+
if path:
|
|
101
|
+
|
|
102
|
+
pattern = f"{path}%"
|
|
103
|
+
cursor.execute("""
|
|
104
|
+
SELECT path, size FROM files
|
|
105
|
+
WHERE path LIKE ?
|
|
106
|
+
ORDER BY path
|
|
107
|
+
""", (pattern,))
|
|
108
|
+
else:
|
|
109
|
+
# Root: get all files
|
|
110
|
+
cursor.execute("SELECT path, size FROM files ORDER BY path")
|
|
111
|
+
|
|
112
|
+
all_files = cursor.fetchall()
|
|
113
|
+
|
|
114
|
+
# Build immediate children
|
|
115
|
+
children = {} # Use dict to dedupe (key = child_path)
|
|
116
|
+
|
|
117
|
+
for file_row in all_files:
|
|
118
|
+
file_path = file_row['path']
|
|
119
|
+
|
|
120
|
+
# Strip the parent path prefix
|
|
121
|
+
if path:
|
|
122
|
+
if not file_path.startswith(path):
|
|
123
|
+
continue
|
|
124
|
+
relative = file_path[len(path):]
|
|
125
|
+
else:
|
|
126
|
+
relative = file_path
|
|
127
|
+
|
|
128
|
+
# Find next segment (up to first /)
|
|
129
|
+
slash_pos = relative.find('/')
|
|
130
|
+
|
|
131
|
+
if slash_pos == -1:
|
|
132
|
+
# No slash → immediate file child
|
|
133
|
+
child_name = relative
|
|
134
|
+
child_path = file_path
|
|
135
|
+
|
|
136
|
+
if child_path not in children:
|
|
137
|
+
children[child_path] = {
|
|
138
|
+
"name": child_name,
|
|
139
|
+
"path": child_path,
|
|
140
|
+
"kind": "file",
|
|
141
|
+
"size": file_row['size'],
|
|
142
|
+
"symbol_count": 0
|
|
143
|
+
}
|
|
144
|
+
else:
|
|
145
|
+
# Has slash → directory child
|
|
146
|
+
child_name = relative[:slash_pos]
|
|
147
|
+
child_path = path + child_name + '/'
|
|
148
|
+
|
|
149
|
+
if child_path not in children:
|
|
150
|
+
children[child_path] = {
|
|
151
|
+
"name": child_name,
|
|
152
|
+
"path": child_path,
|
|
153
|
+
"kind": "dir",
|
|
154
|
+
"children_count": 0,
|
|
155
|
+
"symbol_count": 0
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# Now populate counts
|
|
159
|
+
for child_path, child_data in children.items():
|
|
160
|
+
if child_data["kind"] == "file":
|
|
161
|
+
# Count symbols in this file
|
|
162
|
+
cursor.execute("""
|
|
163
|
+
SELECT COUNT(*) as count FROM symbols WHERE file_path = ?
|
|
164
|
+
""", (child_path,))
|
|
165
|
+
child_data["symbol_count"] = cursor.fetchone()['count']
|
|
166
|
+
|
|
167
|
+
else: # dir
|
|
168
|
+
# Count symbols recursively under this directory
|
|
169
|
+
cursor.execute("""
|
|
170
|
+
SELECT COUNT(*) as count FROM symbols
|
|
171
|
+
WHERE file_path LIKE ?
|
|
172
|
+
""", (f"{child_path}%",))
|
|
173
|
+
child_data["symbol_count"] = cursor.fetchone()['count']
|
|
174
|
+
|
|
175
|
+
# Count immediate children of this directory
|
|
176
|
+
child_children = self._count_immediate_children(cursor, child_path)
|
|
177
|
+
child_data["children_count"] = child_children
|
|
178
|
+
|
|
179
|
+
# Convert to list and sort: directories first, then alphabetically
|
|
180
|
+
result = list(children.values())
|
|
181
|
+
result.sort(key=lambda x: (x["kind"] == "file", x["name"]))
|
|
182
|
+
|
|
183
|
+
return result
|
|
184
|
+
|
|
185
|
+
def _count_immediate_children(self, cursor, parent_path: str) -> int:
|
|
186
|
+
"""Count immediate children of a directory (helper for get_children)"""
|
|
187
|
+
cursor.execute("""
|
|
188
|
+
SELECT path FROM files WHERE path LIKE ?
|
|
189
|
+
""", (f"{parent_path}%",))
|
|
190
|
+
|
|
191
|
+
files = cursor.fetchall()
|
|
192
|
+
children = set()
|
|
193
|
+
|
|
194
|
+
for row in files:
|
|
195
|
+
file_path = row['path']
|
|
196
|
+
relative = file_path[len(parent_path):]
|
|
197
|
+
|
|
198
|
+
slash_pos = relative.find('/')
|
|
199
|
+
if slash_pos == -1:
|
|
200
|
+
children.add(relative)
|
|
201
|
+
else:
|
|
202
|
+
children.add(relative[:slash_pos])
|
|
203
|
+
|
|
204
|
+
return len(children)
|
|
205
|
+
|
|
206
|
+
def search_file(self, query: str) -> List[Dict]:
|
|
207
|
+
"""
|
|
208
|
+
Search for files by name or path (like Ctrl+P in VSCode).
|
|
209
|
+
|
|
210
|
+
Fuzzy matching:
|
|
211
|
+
- "pow" → matches "power.c", "power.h", "mp1/src/app/power.c"
|
|
212
|
+
- "app/pow" → matches "mp1/src/app/power.c"
|
|
213
|
+
- Case-insensitive
|
|
214
|
+
|
|
215
|
+
Returns ranked results (best matches first):
|
|
216
|
+
- Exact filename match (highest priority)
|
|
217
|
+
- Filename contains query
|
|
218
|
+
- Path contains query
|
|
219
|
+
- Shorter paths ranked higher
|
|
220
|
+
|
|
221
|
+
Each result includes:
|
|
222
|
+
- name: filename only
|
|
223
|
+
- path: full relative path
|
|
224
|
+
- size: file size
|
|
225
|
+
- symbol_count: number of symbols
|
|
226
|
+
"""
|
|
227
|
+
if not query:
|
|
228
|
+
return []
|
|
229
|
+
|
|
230
|
+
with self._get_connection() as conn:
|
|
231
|
+
cursor = conn.cursor()
|
|
232
|
+
|
|
233
|
+
# Search pattern (case-insensitive)
|
|
234
|
+
pattern = f"%{query}%"
|
|
235
|
+
|
|
236
|
+
cursor.execute("""
|
|
237
|
+
SELECT
|
|
238
|
+
f.path,
|
|
239
|
+
f.size,
|
|
240
|
+
COUNT(s.id) as symbol_count
|
|
241
|
+
FROM files f
|
|
242
|
+
LEFT JOIN symbols s ON f.path = s.file_path
|
|
243
|
+
WHERE f.path LIKE ? COLLATE NOCASE
|
|
244
|
+
GROUP BY f.path
|
|
245
|
+
ORDER BY LENGTH(f.path), f.path
|
|
246
|
+
LIMIT 100
|
|
247
|
+
""", (pattern,))
|
|
248
|
+
|
|
249
|
+
results = []
|
|
250
|
+
for row in cursor.fetchall():
|
|
251
|
+
file_path = row['path']
|
|
252
|
+
filename = Path(file_path).name
|
|
253
|
+
|
|
254
|
+
# Calculate match score for ranking
|
|
255
|
+
score = self._calculate_match_score(file_path, filename, query.lower())
|
|
256
|
+
|
|
257
|
+
results.append({
|
|
258
|
+
"name": filename,
|
|
259
|
+
"path": file_path,
|
|
260
|
+
"size": row['size'],
|
|
261
|
+
"symbol_count": row['symbol_count'],
|
|
262
|
+
"score": score
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
# Sort by score (higher = better match)
|
|
266
|
+
results.sort(key=lambda x: (-x['score'], len(x['path']), x['path']))
|
|
267
|
+
|
|
268
|
+
# Remove score from output
|
|
269
|
+
for r in results:
|
|
270
|
+
del r['score']
|
|
271
|
+
|
|
272
|
+
return results
|
|
273
|
+
|
|
274
|
+
def _calculate_match_score(self, path: str, filename: str, query: str) -> int:
|
|
275
|
+
"""
|
|
276
|
+
Calculate match score for ranking search results.
|
|
277
|
+
Higher score = better match.
|
|
278
|
+
"""
|
|
279
|
+
path_lower = path.lower()
|
|
280
|
+
filename_lower = filename.lower()
|
|
281
|
+
|
|
282
|
+
score = 0
|
|
283
|
+
|
|
284
|
+
# Exact filename match (highest priority)
|
|
285
|
+
if filename_lower == query:
|
|
286
|
+
score += 1000
|
|
287
|
+
|
|
288
|
+
# Filename starts with query
|
|
289
|
+
if filename_lower.startswith(query):
|
|
290
|
+
score += 500
|
|
291
|
+
|
|
292
|
+
# Filename contains query
|
|
293
|
+
if query in filename_lower:
|
|
294
|
+
score += 100
|
|
295
|
+
|
|
296
|
+
# Path contains query (lower priority)
|
|
297
|
+
if query in path_lower:
|
|
298
|
+
score += 10
|
|
299
|
+
|
|
300
|
+
# Bonus: shorter paths are better
|
|
301
|
+
score -= len(path) // 10
|
|
302
|
+
|
|
303
|
+
return score
|
|
304
|
+
|
|
305
|
+
def search_symbol_global(self, query: str) -> List[Dict]:
|
|
306
|
+
"""
|
|
307
|
+
Global search across everything (like Ctrl+Shift+F in VSCode).
|
|
308
|
+
|
|
309
|
+
Searches:
|
|
310
|
+
- Symbol names (functions, structs, macros, variables, enums, etc.)
|
|
311
|
+
- File paths
|
|
312
|
+
- Signatures
|
|
313
|
+
|
|
314
|
+
Uses FTS5 for fast full-text search with ranking.
|
|
315
|
+
|
|
316
|
+
Returns ranked results with:
|
|
317
|
+
- id: symbol ID
|
|
318
|
+
- name: symbol name
|
|
319
|
+
- type: symbol type (function, struct, macro, etc.)
|
|
320
|
+
- file_path: where it's defined
|
|
321
|
+
- line_number: line number
|
|
322
|
+
- signature: function signature (if applicable)
|
|
323
|
+
- match_context: what matched (name, file, signature)
|
|
324
|
+
"""
|
|
325
|
+
if not query:
|
|
326
|
+
return []
|
|
327
|
+
|
|
328
|
+
with self._get_connection() as conn:
|
|
329
|
+
cursor = conn.cursor()
|
|
330
|
+
|
|
331
|
+
# Use FTS5 for fast full-text search across name, signature, and file_path
|
|
332
|
+
# FTS5 automatically ranks results by relevance
|
|
333
|
+
# Escape FTS5 special characters: " - ( ) * to prevent syntax errors
|
|
334
|
+
fts_query = self._escape_fts5_query(query)
|
|
335
|
+
|
|
336
|
+
cursor.execute("""
|
|
337
|
+
SELECT
|
|
338
|
+
s.id,
|
|
339
|
+
s.name,
|
|
340
|
+
s.type,
|
|
341
|
+
s.file_path,
|
|
342
|
+
s.line_number,
|
|
343
|
+
s.signature,
|
|
344
|
+
s.scope_kind,
|
|
345
|
+
s.scope_name
|
|
346
|
+
FROM symbols_fts sf
|
|
347
|
+
JOIN symbols s ON sf.rowid = s.id
|
|
348
|
+
WHERE symbols_fts MATCH ?
|
|
349
|
+
ORDER BY rank
|
|
350
|
+
LIMIT 100
|
|
351
|
+
""", (fts_query,))
|
|
352
|
+
|
|
353
|
+
results = []
|
|
354
|
+
for row in cursor.fetchall():
|
|
355
|
+
# Determine what matched (for context)
|
|
356
|
+
match_context = self._get_match_context(
|
|
357
|
+
row['name'],
|
|
358
|
+
row['file_path'],
|
|
359
|
+
row['signature'],
|
|
360
|
+
query.lower()
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
result = {
|
|
364
|
+
"id": row['id'],
|
|
365
|
+
"name": row['name'],
|
|
366
|
+
"type": row['type'],
|
|
367
|
+
"file_path": row['file_path'],
|
|
368
|
+
"line_number": row['line_number'],
|
|
369
|
+
"signature": row['signature'] or "",
|
|
370
|
+
"scope": self._format_scope(row['scope_kind'], row['scope_name']),
|
|
371
|
+
"match_context": match_context
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
results.append(result)
|
|
375
|
+
|
|
376
|
+
return results
|
|
377
|
+
|
|
378
|
+
def _get_match_context(self, name: str, file_path: str, signature: str, query: str) -> str:
|
|
379
|
+
"""Determine what part of the symbol matched the query"""
|
|
380
|
+
name_lower = name.lower()
|
|
381
|
+
file_lower = file_path.lower()
|
|
382
|
+
sig_lower = (signature or "").lower()
|
|
383
|
+
|
|
384
|
+
if query in name_lower:
|
|
385
|
+
return "name"
|
|
386
|
+
elif signature and query in sig_lower:
|
|
387
|
+
return "signature"
|
|
388
|
+
elif query in file_lower:
|
|
389
|
+
return "file"
|
|
390
|
+
else:
|
|
391
|
+
return "fuzzy"
|
|
392
|
+
|
|
393
|
+
def _format_scope(self, scope_kind: str, scope_name: str) -> str:
|
|
394
|
+
"""Format scope for display (e.g., 'struct PowerState')"""
|
|
395
|
+
if scope_kind and scope_name:
|
|
396
|
+
return f"{scope_kind} {scope_name}"
|
|
397
|
+
elif scope_name:
|
|
398
|
+
return scope_name
|
|
399
|
+
else:
|
|
400
|
+
return ""
|
|
401
|
+
|
|
402
|
+
def _escape_fts5_query(self, query: str) -> str:
|
|
403
|
+
"""
|
|
404
|
+
Escape FTS5 special characters to prevent syntax errors.
|
|
405
|
+
FTS5 special chars: " - ( ) *
|
|
406
|
+
We wrap the query in quotes to treat it as a phrase.
|
|
407
|
+
"""
|
|
408
|
+
# Remove/escape problematic characters
|
|
409
|
+
escaped = query.replace('"', '""') # Escape double quotes
|
|
410
|
+
# Wrap in quotes to treat as literal phrase
|
|
411
|
+
return f'"{escaped}"'
|
|
412
|
+
|
|
413
|
+
def search_symbol_infile(self, query: str, file_path: str) -> List[Dict]:
|
|
414
|
+
"""
|
|
415
|
+
Search symbols within a specific file (like Ctrl+F in VSCode on open file).
|
|
416
|
+
|
|
417
|
+
Use case:
|
|
418
|
+
- User has opened "mp1/src/app/msg.c"
|
|
419
|
+
- User presses Ctrl+F and searches "Isr"
|
|
420
|
+
- Returns all symbols matching "Isr" in that file only
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
query: Search term (e.g., "Isr", "Message", "init")
|
|
424
|
+
file_path: File to search in (e.g., "mp1/src/app/msg.c")
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
List of symbols in the file matching query, ordered by line number:
|
|
428
|
+
- id: symbol ID
|
|
429
|
+
- name: symbol name
|
|
430
|
+
- type: symbol type
|
|
431
|
+
- line_number: where it's defined
|
|
432
|
+
- signature: function signature (if applicable)
|
|
433
|
+
- scope: parent scope (if any)
|
|
434
|
+
"""
|
|
435
|
+
if not query or not file_path:
|
|
436
|
+
return []
|
|
437
|
+
|
|
438
|
+
with self._get_connection() as conn:
|
|
439
|
+
cursor = conn.cursor()
|
|
440
|
+
|
|
441
|
+
# Search for symbols in specific file (case-insensitive partial match)
|
|
442
|
+
pattern = f"%{query}%"
|
|
443
|
+
|
|
444
|
+
cursor.execute("""
|
|
445
|
+
SELECT
|
|
446
|
+
id,
|
|
447
|
+
name,
|
|
448
|
+
type,
|
|
449
|
+
file_path,
|
|
450
|
+
line_number,
|
|
451
|
+
signature,
|
|
452
|
+
scope_kind,
|
|
453
|
+
scope_name
|
|
454
|
+
FROM symbols
|
|
455
|
+
WHERE file_path = ?
|
|
456
|
+
AND (name LIKE ? OR signature LIKE ?)
|
|
457
|
+
ORDER BY line_number
|
|
458
|
+
""", (file_path, pattern, pattern))
|
|
459
|
+
|
|
460
|
+
results = []
|
|
461
|
+
for row in cursor.fetchall():
|
|
462
|
+
# Determine what matched
|
|
463
|
+
name_match = query.lower() in row['name'].lower()
|
|
464
|
+
sig_match = row['signature'] and query.lower() in row['signature'].lower()
|
|
465
|
+
|
|
466
|
+
match_in = "name" if name_match else "signature" if sig_match else "unknown"
|
|
467
|
+
|
|
468
|
+
result = {
|
|
469
|
+
"id": row['id'],
|
|
470
|
+
"name": row['name'],
|
|
471
|
+
"type": row['type'],
|
|
472
|
+
"line_number": row['line_number'],
|
|
473
|
+
"signature": row['signature'] or "",
|
|
474
|
+
"scope": self._format_scope(row['scope_kind'], row['scope_name']),
|
|
475
|
+
"match_in": match_in
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
results.append(result)
|
|
479
|
+
|
|
480
|
+
return results
|