1use crate::builtin_tools::BuiltinTool;
22use crate::lsp::{LspClient, Position};
23use crate::types::{Layer3Result, ToolCategory};
24use async_trait::async_trait;
25use regex::Regex;
26use std::fs;
27use std::path::{Path, PathBuf};
28use std::sync::Arc;
29use tokio::sync::Mutex;
30use tokio::sync::OnceCell;
31
32pub const CODE_ANALYSIS_VERSION: &str = "v2-lsp-integrated";
34
35static GLOBAL_LSP_CLIENT: OnceCell<Arc<Mutex<LspClient>>> = OnceCell::const_new();
37
38async fn get_lsp_client() -> Arc<Mutex<LspClient>> {
40 GLOBAL_LSP_CLIENT
41 .get_or_init(|| async { Arc::new(Mutex::new(LspClient::new())) })
42 .await
43 .clone()
44}
45
46pub struct GoToDefinitionTool;
58
59#[async_trait]
60impl BuiltinTool for GoToDefinitionTool {
61 fn name(&self) -> &str {
62 "go_to_definition"
63 }
64
65 fn description(&self) -> &str {
66 "Find the definition of a symbol at a given location. \
67 [LSP VERSION] Uses Language Server Protocol for cross-module resolution. \
68 Automatically falls back to regex matching when LSP server is unavailable."
69 }
70
71 fn parameters_schema(&self) -> serde_json::Value {
72 serde_json::json!({
73 "type": "object",
74 "properties": {
75 "file": {
76 "type": "string",
77 "description": "The file path"
78 },
79 "line": {
80 "type": "integer",
81 "description": "Line number (1-based)"
82 },
83 "column": {
84 "type": "integer",
85 "description": "Column number (1-based)"
86 },
87 "symbol": {
88 "type": "string",
89 "description": "Optional: the symbol name to search for"
90 }
91 },
92 "required": ["file", "line", "column"]
93 })
94 }
95
96 fn category(&self) -> ToolCategory {
97 ToolCategory::CodeAnalysis
98 }
99
100 async fn execute(&self, args: serde_json::Value) -> Layer3Result<String> {
101 let file_path = args["file"]
102 .as_str()
103 .ok_or_else(|| anyhow::anyhow!("Missing file parameter"))?;
104
105 let line = args["line"].as_u64().unwrap_or(1) as usize;
106 let column = args["column"].as_u64().unwrap_or(1) as usize;
107 let symbol = args["symbol"].as_str();
108
109 let path = PathBuf::from(file_path);
110
111 match self.execute_with_lsp(&path, line, column).await {
113 Ok(result) => return Ok(result),
114 Err(e) => {
115 tracing::debug!("LSP failed, falling back to regex: {}", e);
116 }
117 }
118
119 self.execute_with_regex(&path, line, column, symbol).await
121 }
122}
123
124impl GoToDefinitionTool {
125 async fn execute_with_lsp(
127 &self,
128 file_path: &Path,
129 line: usize,
130 column: usize,
131 ) -> Layer3Result<String> {
132 let ext = file_path
133 .extension()
134 .and_then(|e| e.to_str())
135 .ok_or_else(|| anyhow::anyhow!("Unknown file extension"))?;
136
137 let language = crate::lsp::server::LanguageServerManager::get_language_from_extension(ext)
138 .ok_or_else(|| anyhow::anyhow!("No LSP server for extension: {}", ext))?;
139
140 let client = get_lsp_client().await;
141 let client = client.lock().await;
142
143 let root_path = file_path.parent().unwrap_or(Path::new("."));
145
146 if !client.is_connected(language).await {
147 client
148 .initialize(language, root_path)
149 .await
150 .map_err(|e| anyhow::anyhow!("Failed to initialize LSP server: {}", e))?;
151 }
152
153 client
155 .open_document(language, file_path)
156 .await
157 .map_err(|e| anyhow::anyhow!("Failed to open document: {}", e))?;
158
159 let position = Position::new((line - 1) as u32, (column - 1) as u32);
161
162 let locations = client
164 .go_to_definition(language, file_path, position)
165 .await
166 .map_err(|e| anyhow::anyhow!("LSP request failed: {}", e))?;
167
168 if locations.is_empty() {
169 return Err(anyhow::anyhow!("No definition found via LSP"));
170 }
171
172 let mut results = Vec::new();
174 for loc in locations {
175 let loc_path = loc
176 .uri
177 .strip_prefix("file://")
178 .unwrap_or(&loc.uri)
179 .strip_prefix('/')
180 .unwrap_or(&loc.uri);
181 results.push(format!(
182 "Definition found in {} at line {}, column {}:\n [LSP cross-module result]",
183 loc_path,
184 loc.range.start.line + 1,
185 loc.range.start.character + 1
186 ));
187 }
188
189 Ok(results.join("\n"))
190 }
191
192 async fn execute_with_regex(
194 &self,
195 file_path: &Path,
196 line: usize,
197 column: usize,
198 symbol: Option<&str>,
199 ) -> Layer3Result<String> {
200 let content = fs::read_to_string(file_path)
202 .map_err(|e| anyhow::anyhow!("Failed to read file {:?}: {}", file_path, e))?;
203
204 let lines: Vec<&str> = content.lines().collect();
205
206 let current_line = lines.get(line - 1).copied().unwrap_or("");
208 let target_symbol = symbol
209 .map(|s| s.to_string())
210 .unwrap_or_else(|| extract_symbol_at_position(current_line, column));
211
212 if target_symbol.is_empty() {
213 return Ok("No symbol found at specified location".to_string());
214 }
215
216 let file_ext = file_path.extension().and_then(|e| e.to_str()).unwrap_or("");
218
219 let definition_patterns = get_definition_patterns(file_ext, &target_symbol);
220
221 for pattern_str in definition_patterns {
223 let pattern =
224 Regex::new(&pattern_str).map_err(|e| anyhow::anyhow!("Invalid regex: {}", e))?;
225
226 for (line_num, line_content) in lines.iter().enumerate() {
228 if pattern.is_match(line_content) {
229 let match_info = pattern.find(line_content).unwrap();
230 return Ok(format!(
231 "Definition found in {} at line {}, column {}:\n{}",
232 file_path.display(),
233 line_num + 1,
234 match_info.start() + 1,
235 line_content.trim()
236 ));
237 }
238 }
239
240 let dir = file_path.parent().unwrap_or(Path::new("."));
242 if let Ok(entries) = fs::read_dir(dir) {
243 for entry in entries.flatten() {
244 let entry_path = entry.path();
245 if entry_path.is_file() && entry_path != *file_path {
246 let ext = entry_path
247 .extension()
248 .and_then(|e| e.to_str())
249 .unwrap_or("");
250 if ext == file_ext {
251 if let Ok(other_content) = fs::read_to_string(&entry_path) {
252 for (line_num, line_content) in other_content.lines().enumerate() {
253 if pattern.is_match(line_content) {
254 return Ok(format!(
255 "Definition found in {} at line {}:\n{}",
256 entry_path.display(),
257 line_num + 1,
258 line_content.trim()
259 ));
260 }
261 }
262 }
263 }
264 }
265 }
266 }
267 }
268
269 Ok(format!("No definition found for symbol: {}", target_symbol))
270 }
271}
272
273pub struct FindReferencesTool;
281
282#[async_trait]
283impl BuiltinTool for FindReferencesTool {
284 fn name(&self) -> &str {
285 "find_references"
286 }
287
288 fn description(&self) -> &str {
289 "Find all references to a symbol at a given location. \
290 [LSP VERSION] Uses Language Server Protocol for project-wide search. \
291 Automatically falls back to regex matching when LSP server is unavailable."
292 }
293
294 fn parameters_schema(&self) -> serde_json::Value {
295 serde_json::json!({
296 "type": "object",
297 "properties": {
298 "file": {
299 "type": "string",
300 "description": "The file path"
301 },
302 "line": {
303 "type": "integer",
304 "description": "Line number (1-based)"
305 },
306 "column": {
307 "type": "integer",
308 "description": "Column number (1-based)"
309 },
310 "symbol": {
311 "type": "string",
312 "description": "Optional: the symbol name to search for"
313 },
314 "include_declaration": {
315 "type": "boolean",
316 "description": "Include declaration in results (default: true)"
317 }
318 },
319 "required": ["file", "line", "column"]
320 })
321 }
322
323 fn category(&self) -> ToolCategory {
324 ToolCategory::CodeAnalysis
325 }
326
327 async fn execute(&self, args: serde_json::Value) -> Layer3Result<String> {
328 let file_path = args["file"]
329 .as_str()
330 .ok_or_else(|| anyhow::anyhow!("Missing file parameter"))?;
331
332 let line = args["line"].as_u64().unwrap_or(1) as usize;
333 let column = args["column"].as_u64().unwrap_or(1) as usize;
334 let symbol = args["symbol"].as_str();
335 let include_declaration = args["include_declaration"].as_bool().unwrap_or(true);
336
337 let path = PathBuf::from(file_path);
338
339 match self
341 .execute_with_lsp(&path, line, column, include_declaration)
342 .await
343 {
344 Ok(result) => return Ok(result),
345 Err(e) => {
346 tracing::debug!("LSP failed, falling back to regex: {}", e);
347 }
348 }
349
350 self.execute_with_regex(&path, line, column, symbol, include_declaration)
352 .await
353 }
354}
355
356impl FindReferencesTool {
357 async fn execute_with_lsp(
359 &self,
360 file_path: &Path,
361 line: usize,
362 column: usize,
363 include_declaration: bool,
364 ) -> Layer3Result<String> {
365 let ext = file_path
366 .extension()
367 .and_then(|e| e.to_str())
368 .ok_or_else(|| anyhow::anyhow!("Unknown file extension"))?;
369
370 let language = crate::lsp::server::LanguageServerManager::get_language_from_extension(ext)
371 .ok_or_else(|| anyhow::anyhow!("No LSP server for extension: {}", ext))?;
372
373 let client = get_lsp_client().await;
374 let client = client.lock().await;
375
376 let root_path = file_path.parent().unwrap_or(Path::new("."));
377
378 if !client.is_connected(language).await {
379 client
380 .initialize(language, root_path)
381 .await
382 .map_err(|e| anyhow::anyhow!("Failed to initialize LSP server: {}", e))?;
383 }
384
385 client
386 .open_document(language, file_path)
387 .await
388 .map_err(|e| anyhow::anyhow!("Failed to open document: {}", e))?;
389
390 let position = Position::new((line - 1) as u32, (column - 1) as u32);
391
392 let locations = client
393 .find_references(language, file_path, position, include_declaration)
394 .await
395 .map_err(|e| anyhow::anyhow!("LSP request failed: {}", e))?;
396
397 if locations.is_empty() {
398 return Err(anyhow::anyhow!("No references found via LSP"));
399 }
400
401 let mut results = Vec::new();
402 for loc in locations {
403 let loc_path = loc
404 .uri
405 .strip_prefix("file://")
406 .unwrap_or(&loc.uri)
407 .strip_prefix('/')
408 .unwrap_or(&loc.uri);
409 results.push(format!(
410 "{}:{}:{}",
411 loc_path,
412 loc.range.start.line + 1,
413 loc.range.start.character + 1
414 ));
415 }
416
417 Ok(format!(
418 "Found {} references (LSP project-wide search):\n{}",
419 results.len(),
420 results.join("\n")
421 ))
422 }
423
424 async fn execute_with_regex(
426 &self,
427 file_path: &Path,
428 line: usize,
429 column: usize,
430 symbol: Option<&str>,
431 include_declaration: bool,
432 ) -> Layer3Result<String> {
433 let content = fs::read_to_string(file_path)
434 .map_err(|e| anyhow::anyhow!("Failed to read file {:?}: {}", file_path, e))?;
435
436 let lines: Vec<&str> = content.lines().collect();
437 let current_line = lines.get(line - 1).copied().unwrap_or("");
438
439 let target_symbol = symbol
440 .map(|s| s.to_string())
441 .unwrap_or_else(|| extract_symbol_at_position(current_line, column));
442
443 if target_symbol.is_empty() {
444 return Ok("No symbol found at specified location".to_string());
445 }
446
447 let reference_pattern = Regex::new(&format!(r"\b{}\b", target_symbol))
448 .map_err(|e| anyhow::anyhow!("Invalid regex for symbol: {}", e))?;
449
450 let mut results = Vec::new();
451
452 for (line_num, line_content) in lines.iter().enumerate() {
454 if reference_pattern.is_match(line_content) {
455 let is_declaration = is_definition_line(line_content, &target_symbol);
456 if include_declaration || !is_declaration {
457 let matches: Vec<_> = reference_pattern.find_iter(line_content).collect();
458 for m in matches {
459 results.push(format!(
460 "{}:{}:{} - {}",
461 file_path.display(),
462 line_num + 1,
463 m.start() + 1,
464 line_content.trim()
465 ));
466 }
467 }
468 }
469 }
470
471 let dir = file_path.parent().unwrap_or(Path::new("."));
473 let file_ext = file_path.extension().and_then(|e| e.to_str()).unwrap_or("");
474
475 if let Ok(entries) = fs::read_dir(dir) {
476 for entry in entries.flatten() {
477 let entry_path = entry.path();
478 if entry_path.is_file() && entry_path != *file_path {
479 let ext = entry_path
480 .extension()
481 .and_then(|e| e.to_str())
482 .unwrap_or("");
483 if ext == file_ext {
484 if let Ok(other_content) = fs::read_to_string(&entry_path) {
485 for (line_num, line_content) in other_content.lines().enumerate() {
486 if reference_pattern.is_match(line_content) {
487 let is_decl = is_definition_line(line_content, &target_symbol);
488 if include_declaration || !is_decl {
489 let matches: Vec<_> =
490 reference_pattern.find_iter(line_content).collect();
491 for m in matches {
492 results.push(format!(
493 "{}:{}:{} - {}",
494 entry_path.display(),
495 line_num + 1,
496 m.start() + 1,
497 line_content.trim()
498 ));
499 }
500 }
501 }
502 }
503 }
504 }
505 }
506 }
507 }
508
509 if results.is_empty() {
510 Ok(format!("No references found for symbol: {}", target_symbol))
511 } else {
512 Ok(format!(
513 "Found {} references:\n{}",
514 results.len(),
515 results.join("\n")
516 ))
517 }
518 }
519}
520
521pub struct GetHoverTool;
529
530#[async_trait]
531impl BuiltinTool for GetHoverTool {
532 fn name(&self) -> &str {
533 "get_hover"
534 }
535
536 fn description(&self) -> &str {
537 "Get type information and documentation for a symbol at a given location. \
538 [LSP VERSION] Uses Language Server Protocol for accurate type information."
539 }
540
541 fn parameters_schema(&self) -> serde_json::Value {
542 serde_json::json!({
543 "type": "object",
544 "properties": {
545 "file": {
546 "type": "string",
547 "description": "The file path"
548 },
549 "line": {
550 "type": "integer",
551 "description": "Line number (1-based)"
552 },
553 "column": {
554 "type": "integer",
555 "description": "Column number (1-based)"
556 }
557 },
558 "required": ["file", "line", "column"]
559 })
560 }
561
562 fn category(&self) -> ToolCategory {
563 ToolCategory::CodeAnalysis
564 }
565
566 async fn execute(&self, args: serde_json::Value) -> Layer3Result<String> {
567 let file_path = args["file"]
568 .as_str()
569 .ok_or_else(|| anyhow::anyhow!("Missing file parameter"))?;
570
571 let line = args["line"].as_u64().unwrap_or(1) as usize;
572 let column = args["column"].as_u64().unwrap_or(1) as usize;
573
574 let path = PathBuf::from(file_path);
575
576 match self.execute_with_lsp(&path, line, column).await {
578 Ok(result) => return Ok(result),
579 Err(e) => {
580 tracing::debug!("LSP failed, falling back to basic info: {}", e);
581 }
582 }
583
584 self.execute_basic(&path, line, column).await
586 }
587}
588
589impl GetHoverTool {
590 async fn execute_with_lsp(
591 &self,
592 file_path: &Path,
593 line: usize,
594 column: usize,
595 ) -> Layer3Result<String> {
596 let ext = file_path
597 .extension()
598 .and_then(|e| e.to_str())
599 .ok_or_else(|| anyhow::anyhow!("Unknown file extension"))?;
600
601 let language = crate::lsp::server::LanguageServerManager::get_language_from_extension(ext)
602 .ok_or_else(|| anyhow::anyhow!("No LSP server for extension: {}", ext))?;
603
604 let client = get_lsp_client().await;
605 let client = client.lock().await;
606
607 let root_path = file_path.parent().unwrap_or(Path::new("."));
608
609 if !client.is_connected(language).await {
610 client
611 .initialize(language, root_path)
612 .await
613 .map_err(|e| anyhow::anyhow!("Failed to initialize LSP server: {}", e))?;
614 }
615
616 client
617 .open_document(language, file_path)
618 .await
619 .map_err(|e| anyhow::anyhow!("Failed to open document: {}", e))?;
620
621 let position = Position::new((line - 1) as u32, (column - 1) as u32);
622
623 let hover = client
624 .get_hover(language, file_path, position)
625 .await
626 .map_err(|e| anyhow::anyhow!("LSP request failed: {}", e))?
627 .ok_or_else(|| anyhow::anyhow!("No hover information available"))?;
628
629 let content = match hover.contents {
631 crate::lsp::HoverContents::Markup(markup) => {
632 format!(
633 "```{}\n{}\n```",
634 match markup.kind {
635 crate::lsp::MarkupKind::PlainText => "",
636 crate::lsp::MarkupKind::Markdown => "markdown",
637 },
638 markup.value
639 )
640 }
641 crate::lsp::HoverContents::String(s) => s,
642 crate::lsp::HoverContents::Array(arr) => arr
643 .iter()
644 .map(|item| match item {
645 crate::lsp::MarkedString::String(s) => s.clone(),
646 crate::lsp::MarkedString::LanguageString(ls) => {
647 format!("```{}\n{}\n```", ls.language, ls.value)
648 }
649 })
650 .collect::<Vec<_>>()
651 .join("\n\n"),
652 };
653
654 Ok(content)
655 }
656
657 async fn execute_basic(
658 &self,
659 file_path: &Path,
660 line: usize,
661 column: usize,
662 ) -> Layer3Result<String> {
663 let content = fs::read_to_string(file_path)
664 .map_err(|e| anyhow::anyhow!("Failed to read file: {}", e))?;
665
666 let lines: Vec<&str> = content.lines().collect();
667 let current_line = lines.get(line - 1).copied().unwrap_or("");
668
669 let symbol = extract_symbol_at_position(current_line, column);
670
671 if symbol.is_empty() {
672 return Ok(format!("Line {}: {}", line, current_line.trim()));
673 }
674
675 Ok(format!(
676 "**{}**\n\n```\n{}\n```",
677 symbol,
678 current_line.trim()
679 ))
680 }
681}
682
683pub struct RenameSymbolTool;
691
692#[async_trait]
693impl BuiltinTool for RenameSymbolTool {
694 fn name(&self) -> &str {
695 "rename_symbol"
696 }
697
698 fn description(&self) -> &str {
699 "Rename a symbol across the entire project. \
700 [LSP VERSION] Uses Language Server Protocol for project-wide refactoring."
701 }
702
703 fn parameters_schema(&self) -> serde_json::Value {
704 serde_json::json!({
705 "type": "object",
706 "properties": {
707 "file": {
708 "type": "string",
709 "description": "The file path"
710 },
711 "line": {
712 "type": "integer",
713 "description": "Line number (1-based)"
714 },
715 "column": {
716 "type": "integer",
717 "description": "Column number (1-based)"
718 },
719 "new_name": {
720 "type": "string",
721 "description": "The new name for the symbol"
722 },
723 "dry_run": {
724 "type": "boolean",
725 "description": "If true, only preview changes without applying them (default: true)"
726 }
727 },
728 "required": ["file", "line", "column", "new_name"]
729 })
730 }
731
732 fn category(&self) -> ToolCategory {
733 ToolCategory::CodeAnalysis
734 }
735
736 fn requires_confirmation(&self) -> bool {
737 true
738 }
739
740 async fn execute(&self, args: serde_json::Value) -> Layer3Result<String> {
741 let file_path = args["file"]
742 .as_str()
743 .ok_or_else(|| anyhow::anyhow!("Missing file parameter"))?;
744
745 let line = args["line"].as_u64().unwrap_or(1) as usize;
746 let column = args["column"].as_u64().unwrap_or(1) as usize;
747 let new_name = args["new_name"]
748 .as_str()
749 .ok_or_else(|| anyhow::anyhow!("Missing new_name parameter"))?;
750 let dry_run = args["dry_run"].as_bool().unwrap_or(true);
751
752 let path = PathBuf::from(file_path);
753
754 let ext = path
756 .extension()
757 .and_then(|e| e.to_str())
758 .ok_or_else(|| anyhow::anyhow!("Unknown file extension"))?;
759
760 let language = crate::lsp::server::LanguageServerManager::get_language_from_extension(ext)
761 .ok_or_else(|| anyhow::anyhow!("No LSP server for extension: {}", ext))?;
762
763 let client = get_lsp_client().await;
764 let client = client.lock().await;
765
766 let root_path = path.parent().unwrap_or(Path::new("."));
767
768 if !client.is_connected(language).await {
769 client
770 .initialize(language, root_path)
771 .await
772 .map_err(|e| anyhow::anyhow!("Failed to initialize LSP server: {}", e))?;
773 }
774
775 client
776 .open_document(language, &path)
777 .await
778 .map_err(|e| anyhow::anyhow!("Failed to open document: {}", e))?;
779
780 let position = Position::new((line - 1) as u32, (column - 1) as u32);
781
782 let workspace_edit = client
783 .rename_symbol(language, &path, position, new_name)
784 .await
785 .map_err(|e| anyhow::anyhow!("LSP rename request failed: {}", e))?
786 .ok_or_else(|| anyhow::anyhow!("Cannot rename symbol at this location"))?;
787
788 let mut changes = Vec::new();
790
791 if let Some(ref change_map) = workspace_edit.changes {
792 for (uri, edits) in change_map {
793 let file = uri
794 .strip_prefix("file://")
795 .unwrap_or(uri.as_str())
796 .strip_prefix('/')
797 .unwrap_or(uri.as_str());
798 for edit in edits {
799 changes.push(format!(
800 "{}:{}:{} -> {}",
801 file,
802 edit.range.start.line + 1,
803 edit.range.start.character + 1,
804 edit.new_text
805 ));
806 }
807 }
808 }
809
810 if dry_run {
811 Ok(format!(
812 "Preview: {} changes would be made to rename symbol to '{}':\n{}",
813 changes.len(),
814 new_name,
815 changes.join("\n")
816 ))
817 } else {
818 self.apply_changes(&workspace_edit)?;
820 Ok(format!(
821 "Successfully renamed symbol to '{}' ({} changes applied)",
822 new_name,
823 changes.len()
824 ))
825 }
826 }
827}
828
829impl RenameSymbolTool {
830 fn apply_changes(&self, workspace_edit: &crate::lsp::WorkspaceEdit) -> Layer3Result<()> {
831 if let Some(changes) = &workspace_edit.changes {
832 for (uri, edits) in changes {
833 let file_path = uri
834 .strip_prefix("file://")
835 .unwrap_or(uri.as_str())
836 .strip_prefix('/')
837 .unwrap_or(uri.as_str());
838
839 let file_path = if cfg!(windows) {
840 file_path.replace('/', "\\")
841 } else {
842 file_path.to_string()
843 };
844
845 let content = fs::read_to_string(&file_path)?;
846 let mut lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
847
848 let mut sorted_edits = edits.clone();
850 sorted_edits.sort_by(|a, b| {
851 b.range
852 .start
853 .line
854 .cmp(&a.range.start.line)
855 .then(b.range.start.character.cmp(&a.range.start.character))
856 });
857
858 for edit in sorted_edits {
859 let line_idx = edit.range.start.line as usize;
860 if line_idx < lines.len() {
861 let line = &mut lines[line_idx];
862 let start = edit.range.start.character as usize;
863 let end = edit.range.end.character as usize;
864
865 if end <= line.len() {
866 line.replace_range(start..end, &edit.new_text);
867 }
868 }
869 }
870
871 fs::write(&file_path, lines.join("\n"))?;
872 }
873 }
874
875 Ok(())
876 }
877}
878
879fn extract_symbol_at_position(line: &str, column: usize) -> String {
885 let line_bytes = line.as_bytes();
886 if column == 0 || column > line.len() {
887 return String::new();
888 }
889
890 let start = line_bytes[..column - 1]
892 .iter()
893 .rposition(|&b| !is_identifier_char(b))
894 .map(|p| p + 1)
895 .unwrap_or(0);
896
897 let end = line_bytes[column - 1..]
899 .iter()
900 .position(|&b| !is_identifier_char(b))
901 .map(|p| column - 1 + p)
902 .unwrap_or(line.len());
903
904 line[start..end].to_string()
905}
906
907fn is_identifier_char(b: u8) -> bool {
909 b.is_ascii_alphanumeric() || b == b'_' || b == b'-' || b == b':'
910}
911
912fn is_definition_line(line: &str, symbol: &str) -> bool {
914 let patterns = [
915 Regex::new(&format!(r"\bfn\s+{}\s*\(", symbol)).ok(),
916 Regex::new(&format!(r"\bdef\s+{}\s*\(", symbol)).ok(),
917 Regex::new(&format!(r"\bclass\s+{}", symbol)).ok(),
918 Regex::new(&format!(r"\bstruct\s+{}", symbol)).ok(),
919 Regex::new(&format!(r"\benum\s+{}", symbol)).ok(),
920 Regex::new(&format!(r"\bimpl\s+{}", symbol)).ok(),
921 Regex::new(&format!(r"\btrait\s+{}", symbol)).ok(),
922 Regex::new(&format!(r"\binterface\s+{}", symbol)).ok(),
923 Regex::new(&format!(r"\btype\s+{}\s*=", symbol)).ok(),
924 Regex::new(&format!(r"\bconst\s+{}", symbol)).ok(),
925 Regex::new(&format!(r"\blet\s+{}\s*=", symbol)).ok(),
926 Regex::new(&format!(r"\bvar\s+{}\s*=", symbol)).ok(),
927 Regex::new(&format!(r"\bpublic\s+{}\s*\(", symbol)).ok(),
928 Regex::new(&format!(r"\bprivate\s+{}\s*\(", symbol)).ok(),
929 ];
930
931 patterns
932 .iter()
933 .any(|p| p.as_ref().is_some_and(|r| r.is_match(line)))
934}
935
936fn get_definition_patterns(file_ext: &str, symbol: &str) -> Vec<String> {
938 match file_ext {
939 "rs" => vec![
940 format!(r"\bfn\s+{}\s*[<(]", symbol),
941 format!(r"\bstruct\s+{}\s*[{{<\s]", symbol),
942 format!(r"\benum\s+{}\s*[{{<\s]", symbol),
943 format!(r"\btrait\s+{}\s*[{{<\s]", symbol),
944 format!(r"\bimpl\s+(?:\w+\s+for\s+)?{}|impl\s+{}", symbol, symbol),
945 format!(r"\btype\s+{}\s*=", symbol),
946 format!(r"\bconst\s+{}\s*:", symbol),
947 format!(r"\bstatic\s+{}\s*:", symbol),
948 format!(r"\bmacro_rules!\s+{}", symbol),
949 ],
950 "py" => vec![
951 format!(r"\bdef\s+{}\s*\(", symbol),
952 format!(r"\bclass\s+{}\s*[:\(]", symbol),
953 format!(r"\basync\s+def\s+{}\s*\(", symbol),
954 ],
955 "js" | "ts" | "tsx" => vec![
956 format!(r"\bfunction\s+{}\s*\(", symbol),
957 format!(r"\bclass\s+{}\s*[{{extends\s]", symbol),
958 format!(r"\bconst\s+{}\s*=", symbol),
959 format!(r"\blet\s+{}\s*=", symbol),
960 format!(r"\bvar\s+{}\s*=", symbol),
961 format!(r"\binterface\s+{}\s*[{{extends\s]", symbol),
962 format!(r"\btype\s+{}\s*=", symbol),
963 format!(r"\bexport\s+(?:default\s+)?(?:function|class)\s+{}", symbol),
964 ],
965 "java" | "kt" => vec![
966 format!(r"\bclass\s+{}\s*[{{extends\s]", symbol),
967 format!(r"\binterface\s+{}\s*[{{extends\s]", symbol),
968 format!(
969 r"\b(?:public|private|protected)\s+(?:static\s+)?(?:\w+\s+)?{}\s*\(",
970 symbol
971 ),
972 format!(r"\benum\s+{}\s*[{{]", symbol),
973 ],
974 "go" => vec![
975 format!(r"\bfunc\s+{}\s*\(", symbol),
976 format!(r"\bfunc\s+\(\w+\s*\*?\w*\)\s+{}\s*\(", symbol),
977 format!(r"\btype\s+{}\s+struct", symbol),
978 format!(r"\btype\s+{}\s+interface", symbol),
979 format!(r"\bvar\s+{}\s*=", symbol),
980 format!(r"\bconst\s+{}\s*=", symbol),
981 ],
982 "c" | "cpp" | "h" | "hpp" => vec![
983 format!(
984 r"\b(?:void|int|char|float|double|auto|struct|class)\s+{}\s*\(",
985 symbol
986 ),
987 format!(r"\bstruct\s+{}\s*[{{]", symbol),
988 format!(r"\bclass\s+{}\s*[{{:]", symbol),
989 format!(r"\btypedef\s+.*\s+{}\s*;", symbol),
990 ],
991 _ => vec![
992 format!(r"\b{}\s*[:=]", symbol),
993 format!(r"\b{}\s*\(", symbol),
994 ],
995 }
996}
997
998#[cfg(test)]
999mod tests {
1000 use super::*;
1001 use serde_json::json;
1002
1003 #[test]
1004 fn test_goto_definition_category() {
1005 let tool = GoToDefinitionTool;
1006 assert_eq!(tool.category(), ToolCategory::CodeAnalysis);
1007 }
1008
1009 #[test]
1010 fn test_find_references_category() {
1011 let tool = FindReferencesTool;
1012 assert_eq!(tool.category(), ToolCategory::CodeAnalysis);
1013 }
1014
1015 #[test]
1016 fn test_get_hover_category() {
1017 let tool = GetHoverTool;
1018 assert_eq!(tool.category(), ToolCategory::CodeAnalysis);
1019 }
1020
1021 #[test]
1022 fn test_rename_symbol_category() {
1023 let tool = RenameSymbolTool;
1024 assert_eq!(tool.category(), ToolCategory::CodeAnalysis);
1025 assert!(tool.requires_confirmation());
1026 }
1027
1028 #[test]
1029 fn test_extract_symbol() {
1030 let line = "let my_variable = 42;";
1031 let symbol = extract_symbol_at_position(line, 10);
1032 assert_eq!(symbol, "my_variable");
1033 }
1034
1035 #[test]
1036 fn test_extract_symbol_empty() {
1037 let line = " = 42;";
1038 let symbol = extract_symbol_at_position(line, 5);
1039 assert_eq!(symbol, "");
1040 }
1041
1042 #[test]
1043 fn test_is_definition_line() {
1044 assert!(is_definition_line("fn my_func() {", "my_func"));
1045 assert!(is_definition_line("struct MyStruct {", "MyStruct"));
1046 assert!(!is_definition_line("my_func();", "my_func"));
1047 }
1048
1049 #[test]
1050 fn test_get_definition_patterns_rust() {
1051 let patterns = get_definition_patterns("rs", "foo");
1052 assert!(patterns.iter().any(|p| p.contains("fn")));
1053 assert!(patterns.iter().any(|p| p.contains("struct")));
1054 }
1055
1056 #[tokio::test]
1057 async fn test_goto_definition_missing_file() {
1058 let tool = GoToDefinitionTool;
1059 let result = tool
1060 .execute(json!({"file": "nonexistent.rs", "line": 1, "column": 1}))
1061 .await;
1062 assert!(result.is_err());
1064 }
1065
1066 #[tokio::test]
1067 async fn test_find_references_missing_file() {
1068 let tool = FindReferencesTool;
1069 let result = tool
1070 .execute(json!({"file": "nonexistent.rs", "line": 1, "column": 1}))
1071 .await;
1072 assert!(result.is_err());
1074 }
1075}