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.

Files changed (86) hide show
  1. mcp_vector_search/__init__.py +10 -0
  2. mcp_vector_search/cli/__init__.py +1 -0
  3. mcp_vector_search/cli/commands/__init__.py +1 -0
  4. mcp_vector_search/cli/commands/auto_index.py +397 -0
  5. mcp_vector_search/cli/commands/chat.py +534 -0
  6. mcp_vector_search/cli/commands/config.py +393 -0
  7. mcp_vector_search/cli/commands/demo.py +358 -0
  8. mcp_vector_search/cli/commands/index.py +762 -0
  9. mcp_vector_search/cli/commands/init.py +658 -0
  10. mcp_vector_search/cli/commands/install.py +869 -0
  11. mcp_vector_search/cli/commands/install_old.py +700 -0
  12. mcp_vector_search/cli/commands/mcp.py +1254 -0
  13. mcp_vector_search/cli/commands/reset.py +393 -0
  14. mcp_vector_search/cli/commands/search.py +796 -0
  15. mcp_vector_search/cli/commands/setup.py +1133 -0
  16. mcp_vector_search/cli/commands/status.py +584 -0
  17. mcp_vector_search/cli/commands/uninstall.py +404 -0
  18. mcp_vector_search/cli/commands/visualize/__init__.py +39 -0
  19. mcp_vector_search/cli/commands/visualize/cli.py +265 -0
  20. mcp_vector_search/cli/commands/visualize/exporters/__init__.py +12 -0
  21. mcp_vector_search/cli/commands/visualize/exporters/html_exporter.py +33 -0
  22. mcp_vector_search/cli/commands/visualize/exporters/json_exporter.py +29 -0
  23. mcp_vector_search/cli/commands/visualize/graph_builder.py +709 -0
  24. mcp_vector_search/cli/commands/visualize/layout_engine.py +469 -0
  25. mcp_vector_search/cli/commands/visualize/server.py +201 -0
  26. mcp_vector_search/cli/commands/visualize/state_manager.py +428 -0
  27. mcp_vector_search/cli/commands/visualize/templates/__init__.py +16 -0
  28. mcp_vector_search/cli/commands/visualize/templates/base.py +218 -0
  29. mcp_vector_search/cli/commands/visualize/templates/scripts.py +3670 -0
  30. mcp_vector_search/cli/commands/visualize/templates/styles.py +779 -0
  31. mcp_vector_search/cli/commands/visualize.py.original +2536 -0
  32. mcp_vector_search/cli/commands/watch.py +287 -0
  33. mcp_vector_search/cli/didyoumean.py +520 -0
  34. mcp_vector_search/cli/export.py +320 -0
  35. mcp_vector_search/cli/history.py +295 -0
  36. mcp_vector_search/cli/interactive.py +342 -0
  37. mcp_vector_search/cli/main.py +484 -0
  38. mcp_vector_search/cli/output.py +414 -0
  39. mcp_vector_search/cli/suggestions.py +375 -0
  40. mcp_vector_search/config/__init__.py +1 -0
  41. mcp_vector_search/config/constants.py +24 -0
  42. mcp_vector_search/config/defaults.py +200 -0
  43. mcp_vector_search/config/settings.py +146 -0
  44. mcp_vector_search/core/__init__.py +1 -0
  45. mcp_vector_search/core/auto_indexer.py +298 -0
  46. mcp_vector_search/core/config_utils.py +394 -0
  47. mcp_vector_search/core/connection_pool.py +360 -0
  48. mcp_vector_search/core/database.py +1237 -0
  49. mcp_vector_search/core/directory_index.py +318 -0
  50. mcp_vector_search/core/embeddings.py +294 -0
  51. mcp_vector_search/core/exceptions.py +89 -0
  52. mcp_vector_search/core/factory.py +318 -0
  53. mcp_vector_search/core/git_hooks.py +345 -0
  54. mcp_vector_search/core/indexer.py +1002 -0
  55. mcp_vector_search/core/llm_client.py +453 -0
  56. mcp_vector_search/core/models.py +294 -0
  57. mcp_vector_search/core/project.py +350 -0
  58. mcp_vector_search/core/scheduler.py +330 -0
  59. mcp_vector_search/core/search.py +952 -0
  60. mcp_vector_search/core/watcher.py +322 -0
  61. mcp_vector_search/mcp/__init__.py +5 -0
  62. mcp_vector_search/mcp/__main__.py +25 -0
  63. mcp_vector_search/mcp/server.py +752 -0
  64. mcp_vector_search/parsers/__init__.py +8 -0
  65. mcp_vector_search/parsers/base.py +296 -0
  66. mcp_vector_search/parsers/dart.py +605 -0
  67. mcp_vector_search/parsers/html.py +413 -0
  68. mcp_vector_search/parsers/javascript.py +643 -0
  69. mcp_vector_search/parsers/php.py +694 -0
  70. mcp_vector_search/parsers/python.py +502 -0
  71. mcp_vector_search/parsers/registry.py +223 -0
  72. mcp_vector_search/parsers/ruby.py +678 -0
  73. mcp_vector_search/parsers/text.py +186 -0
  74. mcp_vector_search/parsers/utils.py +265 -0
  75. mcp_vector_search/py.typed +1 -0
  76. mcp_vector_search/utils/__init__.py +42 -0
  77. mcp_vector_search/utils/gitignore.py +250 -0
  78. mcp_vector_search/utils/gitignore_updater.py +212 -0
  79. mcp_vector_search/utils/monorepo.py +339 -0
  80. mcp_vector_search/utils/timing.py +338 -0
  81. mcp_vector_search/utils/version.py +47 -0
  82. mcp_vector_search-0.15.7.dist-info/METADATA +884 -0
  83. mcp_vector_search-0.15.7.dist-info/RECORD +86 -0
  84. mcp_vector_search-0.15.7.dist-info/WHEEL +4 -0
  85. mcp_vector_search-0.15.7.dist-info/entry_points.txt +3 -0
  86. 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"]