mcp-vector-search 0.15.7__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.
Potentially problematic release.
This version of mcp-vector-search might be problematic. Click here for more details.
- mcp_vector_search/__init__.py +10 -0
- mcp_vector_search/cli/__init__.py +1 -0
- mcp_vector_search/cli/commands/__init__.py +1 -0
- mcp_vector_search/cli/commands/auto_index.py +397 -0
- mcp_vector_search/cli/commands/chat.py +534 -0
- mcp_vector_search/cli/commands/config.py +393 -0
- mcp_vector_search/cli/commands/demo.py +358 -0
- mcp_vector_search/cli/commands/index.py +762 -0
- mcp_vector_search/cli/commands/init.py +658 -0
- mcp_vector_search/cli/commands/install.py +869 -0
- mcp_vector_search/cli/commands/install_old.py +700 -0
- mcp_vector_search/cli/commands/mcp.py +1254 -0
- mcp_vector_search/cli/commands/reset.py +393 -0
- mcp_vector_search/cli/commands/search.py +796 -0
- mcp_vector_search/cli/commands/setup.py +1133 -0
- mcp_vector_search/cli/commands/status.py +584 -0
- mcp_vector_search/cli/commands/uninstall.py +404 -0
- mcp_vector_search/cli/commands/visualize/__init__.py +39 -0
- mcp_vector_search/cli/commands/visualize/cli.py +265 -0
- mcp_vector_search/cli/commands/visualize/exporters/__init__.py +12 -0
- mcp_vector_search/cli/commands/visualize/exporters/html_exporter.py +33 -0
- mcp_vector_search/cli/commands/visualize/exporters/json_exporter.py +29 -0
- mcp_vector_search/cli/commands/visualize/graph_builder.py +709 -0
- mcp_vector_search/cli/commands/visualize/layout_engine.py +469 -0
- mcp_vector_search/cli/commands/visualize/server.py +201 -0
- mcp_vector_search/cli/commands/visualize/state_manager.py +428 -0
- mcp_vector_search/cli/commands/visualize/templates/__init__.py +16 -0
- mcp_vector_search/cli/commands/visualize/templates/base.py +218 -0
- mcp_vector_search/cli/commands/visualize/templates/scripts.py +3670 -0
- mcp_vector_search/cli/commands/visualize/templates/styles.py +779 -0
- mcp_vector_search/cli/commands/visualize.py.original +2536 -0
- mcp_vector_search/cli/commands/watch.py +287 -0
- mcp_vector_search/cli/didyoumean.py +520 -0
- mcp_vector_search/cli/export.py +320 -0
- mcp_vector_search/cli/history.py +295 -0
- mcp_vector_search/cli/interactive.py +342 -0
- mcp_vector_search/cli/main.py +484 -0
- mcp_vector_search/cli/output.py +414 -0
- mcp_vector_search/cli/suggestions.py +375 -0
- mcp_vector_search/config/__init__.py +1 -0
- mcp_vector_search/config/constants.py +24 -0
- mcp_vector_search/config/defaults.py +200 -0
- mcp_vector_search/config/settings.py +146 -0
- mcp_vector_search/core/__init__.py +1 -0
- mcp_vector_search/core/auto_indexer.py +298 -0
- mcp_vector_search/core/config_utils.py +394 -0
- mcp_vector_search/core/connection_pool.py +360 -0
- mcp_vector_search/core/database.py +1237 -0
- mcp_vector_search/core/directory_index.py +318 -0
- mcp_vector_search/core/embeddings.py +294 -0
- mcp_vector_search/core/exceptions.py +89 -0
- mcp_vector_search/core/factory.py +318 -0
- mcp_vector_search/core/git_hooks.py +345 -0
- mcp_vector_search/core/indexer.py +1002 -0
- mcp_vector_search/core/llm_client.py +453 -0
- mcp_vector_search/core/models.py +294 -0
- mcp_vector_search/core/project.py +350 -0
- mcp_vector_search/core/scheduler.py +330 -0
- mcp_vector_search/core/search.py +952 -0
- mcp_vector_search/core/watcher.py +322 -0
- mcp_vector_search/mcp/__init__.py +5 -0
- mcp_vector_search/mcp/__main__.py +25 -0
- mcp_vector_search/mcp/server.py +752 -0
- mcp_vector_search/parsers/__init__.py +8 -0
- mcp_vector_search/parsers/base.py +296 -0
- mcp_vector_search/parsers/dart.py +605 -0
- mcp_vector_search/parsers/html.py +413 -0
- mcp_vector_search/parsers/javascript.py +643 -0
- mcp_vector_search/parsers/php.py +694 -0
- mcp_vector_search/parsers/python.py +502 -0
- mcp_vector_search/parsers/registry.py +223 -0
- mcp_vector_search/parsers/ruby.py +678 -0
- mcp_vector_search/parsers/text.py +186 -0
- mcp_vector_search/parsers/utils.py +265 -0
- mcp_vector_search/py.typed +1 -0
- mcp_vector_search/utils/__init__.py +42 -0
- mcp_vector_search/utils/gitignore.py +250 -0
- mcp_vector_search/utils/gitignore_updater.py +212 -0
- mcp_vector_search/utils/monorepo.py +339 -0
- mcp_vector_search/utils/timing.py +338 -0
- mcp_vector_search/utils/version.py +47 -0
- mcp_vector_search-0.15.7.dist-info/METADATA +884 -0
- mcp_vector_search-0.15.7.dist-info/RECORD +86 -0
- mcp_vector_search-0.15.7.dist-info/WHEEL +4 -0
- mcp_vector_search-0.15.7.dist-info/entry_points.txt +3 -0
- mcp_vector_search-0.15.7.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
"""Dart/Flutter parser using Tree-sitter for MCP Vector Search."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
from ..core.models import CodeChunk
|
|
9
|
+
from .base import BaseParser
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DartParser(BaseParser):
|
|
13
|
+
"""Dart/Flutter parser using Tree-sitter for AST-based code analysis."""
|
|
14
|
+
|
|
15
|
+
def __init__(self) -> None:
|
|
16
|
+
"""Initialize Dart parser."""
|
|
17
|
+
super().__init__("dart")
|
|
18
|
+
self._parser = None
|
|
19
|
+
self._language = None
|
|
20
|
+
self._initialize_parser()
|
|
21
|
+
|
|
22
|
+
def _initialize_parser(self) -> None:
|
|
23
|
+
"""Initialize Tree-sitter parser for Dart."""
|
|
24
|
+
try:
|
|
25
|
+
# Try the tree-sitter-language-pack package (maintained alternative)
|
|
26
|
+
from tree_sitter_language_pack import get_language, get_parser
|
|
27
|
+
|
|
28
|
+
# Get the language and parser objects
|
|
29
|
+
self._language = get_language("dart")
|
|
30
|
+
self._parser = get_parser("dart")
|
|
31
|
+
|
|
32
|
+
logger.debug(
|
|
33
|
+
"Dart Tree-sitter parser initialized via tree-sitter-language-pack"
|
|
34
|
+
)
|
|
35
|
+
return
|
|
36
|
+
except Exception as e:
|
|
37
|
+
logger.debug(f"tree-sitter-language-pack failed: {e}")
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
# Fallback to manual tree-sitter setup (requires language binaries)
|
|
41
|
+
|
|
42
|
+
# This would require language binaries to be available
|
|
43
|
+
# For now, we'll skip this and rely on fallback parsing
|
|
44
|
+
logger.debug("Manual tree-sitter setup not implemented yet")
|
|
45
|
+
self._parser = None
|
|
46
|
+
self._language = None
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.debug(f"Manual tree-sitter setup failed: {e}")
|
|
49
|
+
self._parser = None
|
|
50
|
+
self._language = None
|
|
51
|
+
|
|
52
|
+
logger.info(
|
|
53
|
+
"Using fallback regex-based parsing for Dart (Tree-sitter unavailable)"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
async def parse_file(self, file_path: Path) -> list[CodeChunk]:
|
|
57
|
+
"""Parse a Dart file and extract code chunks."""
|
|
58
|
+
try:
|
|
59
|
+
with open(file_path, encoding="utf-8") as f:
|
|
60
|
+
content = f.read()
|
|
61
|
+
return await self.parse_content(content, file_path)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
logger.error(f"Failed to read file {file_path}: {e}")
|
|
64
|
+
return []
|
|
65
|
+
|
|
66
|
+
async def parse_content(self, content: str, file_path: Path) -> list[CodeChunk]:
|
|
67
|
+
"""Parse Dart content and extract code chunks."""
|
|
68
|
+
if not content.strip():
|
|
69
|
+
return []
|
|
70
|
+
|
|
71
|
+
# If Tree-sitter is not available, fall back to simple parsing
|
|
72
|
+
if not self._parser:
|
|
73
|
+
return await self._fallback_parse(content, file_path)
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
# Parse with Tree-sitter
|
|
77
|
+
tree = self._parser.parse(content.encode("utf-8"))
|
|
78
|
+
return self._extract_chunks_from_tree(tree, content, file_path)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logger.warning(f"Tree-sitter parsing failed for {file_path}: {e}")
|
|
81
|
+
return await self._fallback_parse(content, file_path)
|
|
82
|
+
|
|
83
|
+
def _extract_chunks_from_tree(
|
|
84
|
+
self, tree, content: str, file_path: Path
|
|
85
|
+
) -> list[CodeChunk]:
|
|
86
|
+
"""Extract code chunks from Tree-sitter AST."""
|
|
87
|
+
chunks = []
|
|
88
|
+
lines = self._split_into_lines(content)
|
|
89
|
+
|
|
90
|
+
def visit_node(node, current_class=None):
|
|
91
|
+
"""Recursively visit AST nodes."""
|
|
92
|
+
node_type = node.type
|
|
93
|
+
|
|
94
|
+
if node_type in ["function_signature", "method_signature"]:
|
|
95
|
+
chunks.extend(
|
|
96
|
+
self._extract_function(node, lines, file_path, current_class)
|
|
97
|
+
)
|
|
98
|
+
elif node_type == "class_definition":
|
|
99
|
+
class_chunks = self._extract_class(node, lines, file_path)
|
|
100
|
+
chunks.extend(class_chunks)
|
|
101
|
+
|
|
102
|
+
# Visit class methods with class context
|
|
103
|
+
class_name = self._get_node_name(node)
|
|
104
|
+
for child in node.children:
|
|
105
|
+
visit_node(child, class_name)
|
|
106
|
+
elif node_type == "constructor_signature":
|
|
107
|
+
chunks.extend(
|
|
108
|
+
self._extract_constructor(node, lines, file_path, current_class)
|
|
109
|
+
)
|
|
110
|
+
elif node_type == "mixin_declaration":
|
|
111
|
+
chunks.extend(self._extract_mixin(node, lines, file_path))
|
|
112
|
+
elif node_type == "program":
|
|
113
|
+
# Extract module-level code
|
|
114
|
+
module_chunk = self._extract_module_chunk(node, lines, file_path)
|
|
115
|
+
if module_chunk:
|
|
116
|
+
chunks.append(module_chunk)
|
|
117
|
+
|
|
118
|
+
# Visit all children
|
|
119
|
+
for child in node.children:
|
|
120
|
+
visit_node(child)
|
|
121
|
+
else:
|
|
122
|
+
# Visit children for other node types
|
|
123
|
+
for child in node.children:
|
|
124
|
+
visit_node(child, current_class)
|
|
125
|
+
|
|
126
|
+
# Start traversal from root
|
|
127
|
+
visit_node(tree.root_node)
|
|
128
|
+
|
|
129
|
+
# If no specific chunks found, create a single chunk for the whole file
|
|
130
|
+
if not chunks:
|
|
131
|
+
chunks.append(
|
|
132
|
+
self._create_chunk(
|
|
133
|
+
content=content,
|
|
134
|
+
file_path=file_path,
|
|
135
|
+
start_line=1,
|
|
136
|
+
end_line=len(lines),
|
|
137
|
+
chunk_type="module",
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return chunks
|
|
142
|
+
|
|
143
|
+
def _extract_function(
|
|
144
|
+
self, node, lines: list[str], file_path: Path, class_name: str | None = None
|
|
145
|
+
) -> list[CodeChunk]:
|
|
146
|
+
"""Extract function definition as a chunk."""
|
|
147
|
+
chunks = []
|
|
148
|
+
|
|
149
|
+
function_name = self._get_node_name(node)
|
|
150
|
+
start_line = node.start_point[0] + 1
|
|
151
|
+
end_line = node.end_point[0] + 1
|
|
152
|
+
|
|
153
|
+
# Get function content
|
|
154
|
+
content = self._get_line_range(lines, start_line, end_line)
|
|
155
|
+
|
|
156
|
+
# Extract dartdoc if present
|
|
157
|
+
dartdoc = self._extract_dartdoc(node, lines)
|
|
158
|
+
|
|
159
|
+
chunk = self._create_chunk(
|
|
160
|
+
content=content,
|
|
161
|
+
file_path=file_path,
|
|
162
|
+
start_line=start_line,
|
|
163
|
+
end_line=end_line,
|
|
164
|
+
chunk_type="function",
|
|
165
|
+
function_name=function_name,
|
|
166
|
+
class_name=class_name,
|
|
167
|
+
docstring=dartdoc,
|
|
168
|
+
)
|
|
169
|
+
chunks.append(chunk)
|
|
170
|
+
|
|
171
|
+
return chunks
|
|
172
|
+
|
|
173
|
+
def _extract_class(
|
|
174
|
+
self, node, lines: list[str], file_path: Path
|
|
175
|
+
) -> list[CodeChunk]:
|
|
176
|
+
"""Extract class definition as a chunk."""
|
|
177
|
+
chunks = []
|
|
178
|
+
|
|
179
|
+
class_name = self._get_node_name(node)
|
|
180
|
+
start_line = node.start_point[0] + 1
|
|
181
|
+
end_line = node.end_point[0] + 1
|
|
182
|
+
|
|
183
|
+
# Get class content
|
|
184
|
+
content = self._get_line_range(lines, start_line, end_line)
|
|
185
|
+
|
|
186
|
+
# Extract dartdoc if present
|
|
187
|
+
dartdoc = self._extract_dartdoc(node, lines)
|
|
188
|
+
|
|
189
|
+
chunk = self._create_chunk(
|
|
190
|
+
content=content,
|
|
191
|
+
file_path=file_path,
|
|
192
|
+
start_line=start_line,
|
|
193
|
+
end_line=end_line,
|
|
194
|
+
chunk_type="class",
|
|
195
|
+
class_name=class_name,
|
|
196
|
+
docstring=dartdoc,
|
|
197
|
+
)
|
|
198
|
+
chunks.append(chunk)
|
|
199
|
+
|
|
200
|
+
return chunks
|
|
201
|
+
|
|
202
|
+
def _extract_constructor(
|
|
203
|
+
self, node, lines: list[str], file_path: Path, class_name: str | None = None
|
|
204
|
+
) -> list[CodeChunk]:
|
|
205
|
+
"""Extract constructor definition as a chunk."""
|
|
206
|
+
chunks = []
|
|
207
|
+
|
|
208
|
+
constructor_name = self._get_node_name(node) or class_name
|
|
209
|
+
start_line = node.start_point[0] + 1
|
|
210
|
+
end_line = node.end_point[0] + 1
|
|
211
|
+
|
|
212
|
+
# Get constructor content
|
|
213
|
+
content = self._get_line_range(lines, start_line, end_line)
|
|
214
|
+
|
|
215
|
+
# Extract dartdoc if present
|
|
216
|
+
dartdoc = self._extract_dartdoc(node, lines)
|
|
217
|
+
|
|
218
|
+
chunk = self._create_chunk(
|
|
219
|
+
content=content,
|
|
220
|
+
file_path=file_path,
|
|
221
|
+
start_line=start_line,
|
|
222
|
+
end_line=end_line,
|
|
223
|
+
chunk_type="constructor",
|
|
224
|
+
function_name=constructor_name,
|
|
225
|
+
class_name=class_name,
|
|
226
|
+
docstring=dartdoc,
|
|
227
|
+
)
|
|
228
|
+
chunks.append(chunk)
|
|
229
|
+
|
|
230
|
+
return chunks
|
|
231
|
+
|
|
232
|
+
def _extract_mixin(
|
|
233
|
+
self, node, lines: list[str], file_path: Path
|
|
234
|
+
) -> list[CodeChunk]:
|
|
235
|
+
"""Extract mixin definition as a chunk."""
|
|
236
|
+
chunks = []
|
|
237
|
+
|
|
238
|
+
mixin_name = self._get_node_name(node)
|
|
239
|
+
start_line = node.start_point[0] + 1
|
|
240
|
+
end_line = node.end_point[0] + 1
|
|
241
|
+
|
|
242
|
+
# Get mixin content
|
|
243
|
+
content = self._get_line_range(lines, start_line, end_line)
|
|
244
|
+
|
|
245
|
+
# Extract dartdoc if present
|
|
246
|
+
dartdoc = self._extract_dartdoc(node, lines)
|
|
247
|
+
|
|
248
|
+
chunk = self._create_chunk(
|
|
249
|
+
content=content,
|
|
250
|
+
file_path=file_path,
|
|
251
|
+
start_line=start_line,
|
|
252
|
+
end_line=end_line,
|
|
253
|
+
chunk_type="mixin",
|
|
254
|
+
class_name=mixin_name,
|
|
255
|
+
docstring=dartdoc,
|
|
256
|
+
)
|
|
257
|
+
chunks.append(chunk)
|
|
258
|
+
|
|
259
|
+
return chunks
|
|
260
|
+
|
|
261
|
+
def _extract_module_chunk(
|
|
262
|
+
self, node, lines: list[str], file_path: Path
|
|
263
|
+
) -> CodeChunk | None:
|
|
264
|
+
"""Extract module-level code (imports, exports, etc.)."""
|
|
265
|
+
# Look for module-level statements (not inside functions/classes)
|
|
266
|
+
module_lines = []
|
|
267
|
+
|
|
268
|
+
for child in node.children:
|
|
269
|
+
if child.type in ["import_or_export", "library_name"]:
|
|
270
|
+
start_line = child.start_point[0] + 1
|
|
271
|
+
end_line = child.end_point[0] + 1
|
|
272
|
+
import_content = self._get_line_range(lines, start_line, end_line)
|
|
273
|
+
module_lines.append(import_content.strip())
|
|
274
|
+
|
|
275
|
+
if module_lines:
|
|
276
|
+
content = "\n".join(module_lines)
|
|
277
|
+
return self._create_chunk(
|
|
278
|
+
content=content,
|
|
279
|
+
file_path=file_path,
|
|
280
|
+
start_line=1,
|
|
281
|
+
end_line=len(module_lines),
|
|
282
|
+
chunk_type="imports",
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
def _get_node_name(self, node) -> str | None:
|
|
288
|
+
"""Extract name from a named node (function, class, etc.)."""
|
|
289
|
+
for child in node.children:
|
|
290
|
+
if child.type == "identifier":
|
|
291
|
+
return child.text.decode("utf-8")
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
def _extract_dartdoc(self, node, lines: list[str]) -> str | None:
|
|
295
|
+
"""Extract dartdoc from a function or class node."""
|
|
296
|
+
# Look for documentation_comment node before the definition
|
|
297
|
+
start_line = node.start_point[0]
|
|
298
|
+
|
|
299
|
+
# Check a few lines before the node for dartdoc comments
|
|
300
|
+
dartdoc_lines = []
|
|
301
|
+
for i in range(max(0, start_line - 10), start_line):
|
|
302
|
+
line = lines[i].strip()
|
|
303
|
+
if line.startswith("///"):
|
|
304
|
+
dartdoc_lines.append(line[3:].strip())
|
|
305
|
+
elif line and not line.startswith("//"):
|
|
306
|
+
# Stop if we hit non-comment code
|
|
307
|
+
dartdoc_lines = []
|
|
308
|
+
|
|
309
|
+
if dartdoc_lines:
|
|
310
|
+
return " ".join(dartdoc_lines)
|
|
311
|
+
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
async def _fallback_parse(self, content: str, file_path: Path) -> list[CodeChunk]:
|
|
315
|
+
"""Fallback parsing using regex when Tree-sitter is not available."""
|
|
316
|
+
chunks = []
|
|
317
|
+
lines = self._split_into_lines(content)
|
|
318
|
+
|
|
319
|
+
# Enhanced regex patterns for Dart
|
|
320
|
+
# Match: class WidgetName extends StatelessWidget/StatefulWidget
|
|
321
|
+
widget_pattern = re.compile(
|
|
322
|
+
r"^\s*class\s+(\w+)\s+extends\s+(StatelessWidget|StatefulWidget)",
|
|
323
|
+
re.MULTILINE,
|
|
324
|
+
)
|
|
325
|
+
# Match: class ClassName
|
|
326
|
+
class_pattern = re.compile(r"^\s*class\s+(\w+)\s*[{<]", re.MULTILINE)
|
|
327
|
+
# Match: Future<Type> funcName( or Type funcName( or void funcName(
|
|
328
|
+
function_pattern = re.compile(
|
|
329
|
+
r"^\s*(?:Future<[\w<>]+>|void|[\w<>]+)\s+(\w+)\s*\(", re.MULTILINE
|
|
330
|
+
)
|
|
331
|
+
# Match: import 'package:...' or import "..."
|
|
332
|
+
import_pattern = re.compile(r"^\s*import\s+['\"](.+?)['\"]", re.MULTILINE)
|
|
333
|
+
# Match: mixin MixinName
|
|
334
|
+
mixin_pattern = re.compile(r"^\s*mixin\s+(\w+)", re.MULTILINE)
|
|
335
|
+
|
|
336
|
+
# Extract imports first
|
|
337
|
+
imports = []
|
|
338
|
+
for match in import_pattern.finditer(content):
|
|
339
|
+
import_line = match.group(0).strip()
|
|
340
|
+
imports.append(import_line)
|
|
341
|
+
|
|
342
|
+
# Find Widget classes (high priority for Flutter)
|
|
343
|
+
for match in widget_pattern.finditer(content):
|
|
344
|
+
class_name = match.group(1)
|
|
345
|
+
widget_type = match.group(2)
|
|
346
|
+
|
|
347
|
+
# Find the actual line with 'class' by looking for it in the match
|
|
348
|
+
match_text = match.group(0)
|
|
349
|
+
class_pos_in_match = match_text.find("class")
|
|
350
|
+
actual_class_pos = match.start() + class_pos_in_match
|
|
351
|
+
start_line = content[:actual_class_pos].count("\n") + 1
|
|
352
|
+
|
|
353
|
+
# Find end of class (simple heuristic)
|
|
354
|
+
end_line = self._find_class_end(lines, start_line)
|
|
355
|
+
|
|
356
|
+
class_content = self._get_line_range(lines, start_line, end_line)
|
|
357
|
+
|
|
358
|
+
if class_content.strip():
|
|
359
|
+
# Extract dartdoc using regex
|
|
360
|
+
dartdoc = self._extract_dartdoc_regex(lines, start_line)
|
|
361
|
+
|
|
362
|
+
chunk = self._create_chunk(
|
|
363
|
+
content=class_content,
|
|
364
|
+
file_path=file_path,
|
|
365
|
+
start_line=start_line,
|
|
366
|
+
end_line=end_line,
|
|
367
|
+
chunk_type="widget",
|
|
368
|
+
class_name=f"{class_name} ({widget_type})",
|
|
369
|
+
docstring=dartdoc,
|
|
370
|
+
)
|
|
371
|
+
chunk.imports = imports
|
|
372
|
+
chunks.append(chunk)
|
|
373
|
+
|
|
374
|
+
# Extract build method separately for Flutter widgets
|
|
375
|
+
build_method = self._extract_build_method(class_content, start_line)
|
|
376
|
+
if build_method:
|
|
377
|
+
chunks.append(build_method)
|
|
378
|
+
|
|
379
|
+
# Find regular classes (not already captured as widgets)
|
|
380
|
+
widget_class_names = {
|
|
381
|
+
match.group(1) for match in widget_pattern.finditer(content)
|
|
382
|
+
}
|
|
383
|
+
for match in class_pattern.finditer(content):
|
|
384
|
+
class_name = match.group(1)
|
|
385
|
+
|
|
386
|
+
# Skip if already captured as widget
|
|
387
|
+
if class_name in widget_class_names:
|
|
388
|
+
continue
|
|
389
|
+
|
|
390
|
+
# Find the actual line with 'class'
|
|
391
|
+
match_text = match.group(0)
|
|
392
|
+
class_pos_in_match = match_text.find("class")
|
|
393
|
+
actual_class_pos = match.start() + class_pos_in_match
|
|
394
|
+
start_line = content[:actual_class_pos].count("\n") + 1
|
|
395
|
+
|
|
396
|
+
# Find end of class
|
|
397
|
+
end_line = self._find_class_end(lines, start_line)
|
|
398
|
+
|
|
399
|
+
class_content = self._get_line_range(lines, start_line, end_line)
|
|
400
|
+
|
|
401
|
+
if class_content.strip():
|
|
402
|
+
# Extract dartdoc
|
|
403
|
+
dartdoc = self._extract_dartdoc_regex(lines, start_line)
|
|
404
|
+
|
|
405
|
+
chunk = self._create_chunk(
|
|
406
|
+
content=class_content,
|
|
407
|
+
file_path=file_path,
|
|
408
|
+
start_line=start_line,
|
|
409
|
+
end_line=end_line,
|
|
410
|
+
chunk_type="class",
|
|
411
|
+
class_name=class_name,
|
|
412
|
+
docstring=dartdoc,
|
|
413
|
+
)
|
|
414
|
+
chunk.imports = imports
|
|
415
|
+
chunks.append(chunk)
|
|
416
|
+
|
|
417
|
+
# Find mixins
|
|
418
|
+
for match in mixin_pattern.finditer(content):
|
|
419
|
+
mixin_name = match.group(1)
|
|
420
|
+
|
|
421
|
+
match_text = match.group(0)
|
|
422
|
+
mixin_pos_in_match = match_text.find("mixin")
|
|
423
|
+
actual_mixin_pos = match.start() + mixin_pos_in_match
|
|
424
|
+
start_line = content[:actual_mixin_pos].count("\n") + 1
|
|
425
|
+
|
|
426
|
+
# Find end of mixin
|
|
427
|
+
end_line = self._find_class_end(lines, start_line)
|
|
428
|
+
|
|
429
|
+
mixin_content = self._get_line_range(lines, start_line, end_line)
|
|
430
|
+
|
|
431
|
+
if mixin_content.strip():
|
|
432
|
+
dartdoc = self._extract_dartdoc_regex(lines, start_line)
|
|
433
|
+
|
|
434
|
+
chunk = self._create_chunk(
|
|
435
|
+
content=mixin_content,
|
|
436
|
+
file_path=file_path,
|
|
437
|
+
start_line=start_line,
|
|
438
|
+
end_line=end_line,
|
|
439
|
+
chunk_type="mixin",
|
|
440
|
+
class_name=mixin_name,
|
|
441
|
+
docstring=dartdoc,
|
|
442
|
+
)
|
|
443
|
+
chunk.imports = imports
|
|
444
|
+
chunks.append(chunk)
|
|
445
|
+
|
|
446
|
+
# Find functions (including async functions)
|
|
447
|
+
for match in function_pattern.finditer(content):
|
|
448
|
+
function_name = match.group(1)
|
|
449
|
+
|
|
450
|
+
# Skip constructor-like patterns (same name as class)
|
|
451
|
+
if function_name and function_name[0].isupper():
|
|
452
|
+
# Check if it's a class name (constructor)
|
|
453
|
+
if any(function_name == chunk.class_name for chunk in chunks):
|
|
454
|
+
continue
|
|
455
|
+
|
|
456
|
+
# Find the actual line
|
|
457
|
+
match_text = match.group(0)
|
|
458
|
+
# Look for the function name position
|
|
459
|
+
func_name_pos = match_text.rfind(function_name)
|
|
460
|
+
if func_name_pos == -1:
|
|
461
|
+
continue
|
|
462
|
+
actual_func_pos = match.start() + func_name_pos
|
|
463
|
+
start_line = content[:actual_func_pos].count("\n") + 1
|
|
464
|
+
|
|
465
|
+
# Find end of function
|
|
466
|
+
end_line = self._find_function_end(lines, start_line)
|
|
467
|
+
|
|
468
|
+
func_content = self._get_line_range(lines, start_line, end_line)
|
|
469
|
+
|
|
470
|
+
if func_content.strip():
|
|
471
|
+
# Extract dartdoc
|
|
472
|
+
dartdoc = self._extract_dartdoc_regex(lines, start_line)
|
|
473
|
+
|
|
474
|
+
chunk = self._create_chunk(
|
|
475
|
+
content=func_content,
|
|
476
|
+
file_path=file_path,
|
|
477
|
+
start_line=start_line,
|
|
478
|
+
end_line=end_line,
|
|
479
|
+
chunk_type="function",
|
|
480
|
+
function_name=function_name,
|
|
481
|
+
docstring=dartdoc,
|
|
482
|
+
)
|
|
483
|
+
chunk.imports = imports
|
|
484
|
+
chunks.append(chunk)
|
|
485
|
+
|
|
486
|
+
# If no functions or classes found, create chunks for the whole file
|
|
487
|
+
if not chunks:
|
|
488
|
+
chunks.append(
|
|
489
|
+
self._create_chunk(
|
|
490
|
+
content=content,
|
|
491
|
+
file_path=file_path,
|
|
492
|
+
start_line=1,
|
|
493
|
+
end_line=len(lines),
|
|
494
|
+
chunk_type="module",
|
|
495
|
+
)
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
return chunks
|
|
499
|
+
|
|
500
|
+
def _extract_build_method(
|
|
501
|
+
self, class_content: str, class_start_line: int
|
|
502
|
+
) -> CodeChunk | None:
|
|
503
|
+
"""Extract build() method from a Widget class."""
|
|
504
|
+
# Look for Widget build(BuildContext context)
|
|
505
|
+
build_pattern = re.compile(r"^\s*Widget\s+build\s*\(", re.MULTILINE)
|
|
506
|
+
match = build_pattern.search(class_content)
|
|
507
|
+
|
|
508
|
+
if not match:
|
|
509
|
+
return None
|
|
510
|
+
|
|
511
|
+
# Calculate line number within class
|
|
512
|
+
lines_before = class_content[: match.start()].count("\n")
|
|
513
|
+
class_start_line + lines_before
|
|
514
|
+
|
|
515
|
+
# Find end of build method
|
|
516
|
+
class_lines = class_content.splitlines(keepends=True)
|
|
517
|
+
build_start_idx = lines_before
|
|
518
|
+
|
|
519
|
+
# Simple heuristic: find matching braces
|
|
520
|
+
self._find_method_end(class_lines, build_start_idx)
|
|
521
|
+
|
|
522
|
+
return None # Simplified - build method is already in class chunk
|
|
523
|
+
|
|
524
|
+
def _find_function_end(self, lines: list[str], start_line: int) -> int:
|
|
525
|
+
"""Find the end line of a function using brace matching."""
|
|
526
|
+
if start_line > len(lines):
|
|
527
|
+
return len(lines)
|
|
528
|
+
|
|
529
|
+
start_idx = start_line - 1
|
|
530
|
+
if start_idx >= len(lines):
|
|
531
|
+
return len(lines)
|
|
532
|
+
|
|
533
|
+
# For Dart, we need to count braces
|
|
534
|
+
brace_count = 0
|
|
535
|
+
found_opening_brace = False
|
|
536
|
+
|
|
537
|
+
for i in range(start_idx, len(lines)):
|
|
538
|
+
line = lines[i]
|
|
539
|
+
|
|
540
|
+
for char in line:
|
|
541
|
+
if char == "{":
|
|
542
|
+
brace_count += 1
|
|
543
|
+
found_opening_brace = True
|
|
544
|
+
elif char == "}":
|
|
545
|
+
brace_count -= 1
|
|
546
|
+
if found_opening_brace and brace_count == 0:
|
|
547
|
+
return i + 1 # Return 1-based line number
|
|
548
|
+
|
|
549
|
+
# Handle single-line functions (arrow functions)
|
|
550
|
+
if "=>" in line and ";" in line and not found_opening_brace:
|
|
551
|
+
return i + 1
|
|
552
|
+
|
|
553
|
+
return len(lines)
|
|
554
|
+
|
|
555
|
+
def _find_class_end(self, lines: list[str], start_line: int) -> int:
|
|
556
|
+
"""Find the end line of a class using brace matching."""
|
|
557
|
+
return self._find_function_end(lines, start_line)
|
|
558
|
+
|
|
559
|
+
def _find_method_end(self, lines: list[str], start_idx: int) -> int:
|
|
560
|
+
"""Find the end of a method using brace matching."""
|
|
561
|
+
brace_count = 0
|
|
562
|
+
found_opening_brace = False
|
|
563
|
+
|
|
564
|
+
for i in range(start_idx, len(lines)):
|
|
565
|
+
line = lines[i]
|
|
566
|
+
|
|
567
|
+
for char in line:
|
|
568
|
+
if char == "{":
|
|
569
|
+
brace_count += 1
|
|
570
|
+
found_opening_brace = True
|
|
571
|
+
elif char == "}":
|
|
572
|
+
brace_count -= 1
|
|
573
|
+
if found_opening_brace and brace_count == 0:
|
|
574
|
+
return i + 1
|
|
575
|
+
|
|
576
|
+
return len(lines)
|
|
577
|
+
|
|
578
|
+
def _extract_dartdoc_regex(self, lines: list[str], start_line: int) -> str | None:
|
|
579
|
+
"""Extract dartdoc using regex patterns."""
|
|
580
|
+
# Look for /// comments before the definition
|
|
581
|
+
dartdoc_lines = []
|
|
582
|
+
|
|
583
|
+
# Check a few lines before the start_line
|
|
584
|
+
for i in range(max(0, start_line - 15), start_line - 1):
|
|
585
|
+
if i >= len(lines):
|
|
586
|
+
continue
|
|
587
|
+
|
|
588
|
+
line = lines[i].strip()
|
|
589
|
+
if line.startswith("///"):
|
|
590
|
+
dartdoc_lines.append(line[3:].strip())
|
|
591
|
+
elif line and not line.startswith("//") and dartdoc_lines:
|
|
592
|
+
# If we hit non-comment code after finding dartdoc, stop
|
|
593
|
+
break
|
|
594
|
+
elif line and not line.startswith("//") and not dartdoc_lines:
|
|
595
|
+
# Reset if we hit code before finding dartdoc
|
|
596
|
+
dartdoc_lines = []
|
|
597
|
+
|
|
598
|
+
if dartdoc_lines:
|
|
599
|
+
return " ".join(dartdoc_lines)
|
|
600
|
+
|
|
601
|
+
return None
|
|
602
|
+
|
|
603
|
+
def get_supported_extensions(self) -> list[str]:
|
|
604
|
+
"""Get supported file extensions."""
|
|
605
|
+
return [".dart"]
|