1use std::path::Path;
2
3use anyhow::Context;
4use serde_json::{json, Value};
5use tracing::debug;
6
7use crate::lang::go as lang_go;
8use crate::lsp::client::{self, LspClient};
9use crate::lsp::files::FileTracker;
10
11#[derive(Debug, serde::Serialize)]
13pub struct SymbolMatch {
14 pub path: String,
15 pub line: u32,
16 pub kind: String,
17 pub preview: String,
18 #[serde(skip_serializing_if = "Option::is_none")]
20 pub body: Option<String>,
21}
22
23pub async fn find_symbol(
31 name: &str,
32 client: &mut LspClient,
33 project_root: &Path,
34) -> anyhow::Result<Vec<SymbolMatch>> {
35 let params = json!({ "query": name });
36 let request_id = client
37 .transport_mut()
38 .send_request("workspace/symbol", params)
39 .await?;
40
41 let response = client
42 .wait_for_response_public(request_id)
43 .await
44 .context("workspace/symbol request failed")?;
45
46 Ok(parse_symbol_results(&response, name, project_root))
47}
48
49const HEADER_EXTS: &[&str] = &["h", "hpp", "hxx", "hh"];
51
52pub async fn resolve_symbol_location(
60 name: &str,
61 client: &mut LspClient,
62 project_root: &Path,
63) -> anyhow::Result<(std::path::PathBuf, u32, u32)> {
64 let lsp_symbols = find_symbol(name, client, project_root).await?;
65 let symbols = if lsp_symbols.is_empty() {
68 let name_owned = name.to_string();
69 let root = project_root.to_path_buf();
70 tokio::task::spawn_blocking(move || text_search_find_symbol(&name_owned, &root))
71 .await
72 .unwrap_or_default()
73 } else {
74 lsp_symbols
75 };
76 let symbol = symbols
79 .iter()
80 .find(|s| {
81 let ext = std::path::Path::new(&s.path)
82 .extension()
83 .and_then(|e| e.to_str())
84 .unwrap_or("");
85 !HEADER_EXTS.contains(&ext)
86 })
87 .or_else(|| symbols.first())
88 .with_context(|| format!("symbol '{name}' not found"))?;
89
90 let abs_path = project_root.join(&symbol.path);
91 let (line_0, char_0) = find_name_position(&abs_path, symbol.line, name);
92 Ok((abs_path, line_0, char_0))
93}
94
95#[must_use]
101pub fn text_search_find_symbol(name: &str, project_root: &Path) -> Vec<SymbolMatch> {
102 use crate::commands::search::{run as search_run, SearchOptions};
103
104 let opts = SearchOptions {
105 pattern: name.to_string(),
106 path: None,
107 ignore_case: false,
108 word: true,
109 literal: true,
110 context: 0,
111 files_only: false,
112 lang_filter: None,
113 max_matches: 50,
114 };
115
116 let Ok(output) = search_run(&opts, project_root) else {
117 return vec![];
118 };
119
120 output
121 .matches
122 .into_iter()
123 .filter_map(|m| {
124 classify_definition(&m.preview, name).map(|kind| SymbolMatch {
125 kind: kind.to_string(),
126 path: m.path,
127 line: m.line,
128 preview: m.preview,
129 body: None,
130 })
131 })
132 .collect()
133}
134
135#[must_use]
140pub fn text_search_find_refs(name: &str, project_root: &Path) -> Vec<ReferenceMatch> {
141 use crate::commands::search::{run as search_run, SearchOptions};
142
143 let opts = SearchOptions {
144 pattern: name.to_string(),
145 path: None,
146 ignore_case: false,
147 word: true,
148 literal: true,
149 context: 0,
150 files_only: false,
151 lang_filter: None,
152 max_matches: 200,
153 };
154
155 let Ok(output) = search_run(&opts, project_root) else {
156 return vec![];
157 };
158
159 let mut results: Vec<ReferenceMatch> = output
160 .matches
161 .into_iter()
162 .map(|m| {
163 let is_definition = classify_definition(&m.preview, name).is_some();
164 ReferenceMatch {
165 path: m.path,
166 line: m.line,
167 preview: m.preview,
168 is_definition,
169 containing_symbol: None,
170 }
171 })
172 .collect();
173
174 results.sort_by(|a, b| {
176 b.is_definition
177 .cmp(&a.is_definition)
178 .then(a.path.cmp(&b.path))
179 .then(a.line.cmp(&b.line))
180 });
181
182 results
183}
184
185fn classify_definition<'a>(line: &str, name: &str) -> Option<&'a str> {
194 let trimmed = line.trim();
195 let name_pos = trimmed.find(name)?;
196 let word_before = trimmed[..name_pos].split_whitespace().last().unwrap_or("");
197 let kind = match word_before {
198 "const" | "let" | "var" => "constant",
199 "function" | "fn" | "def" | "async" => "function",
200 "class" => "class",
201 "interface" => "interface",
202 "type" => "type_alias",
203 "struct" | "enum" => "struct",
204 _ => return None,
205 };
206 Some(kind)
207}
208
209async fn find_go_receiver_method(
217 name: &str,
218 client: &mut LspClient,
219 project_root: &Path,
220) -> anyhow::Result<Option<(SymbolMatch, String)>> {
221 let Some(dot) = name.find('.') else {
222 return Ok(None);
223 };
224 let receiver = &name[..dot];
225 let method = &name[dot + 1..];
226
227 let params = json!({ "query": method });
228 let request_id = client
229 .transport_mut()
230 .send_request("workspace/symbol", params)
231 .await?;
232 let response = client
233 .wait_for_response_public(request_id)
234 .await
235 .context("workspace/symbol request failed")?;
236
237 let Some(items) = response.as_array() else {
238 return Ok(None);
239 };
240
241 debug!(
242 "find_go_receiver_method: got {} items for query '{method}'",
243 items.len()
244 );
245 for item in items {
246 let sym_name = item.get("name").and_then(Value::as_str).unwrap_or_default();
247 let matches = crate::lang::go::receiver_method_matches(sym_name, receiver, method);
248 debug!(" sym_name={sym_name:?} receiver_method_matches({receiver},{method})={matches}");
249 if !matches {
250 continue;
251 }
252 let (path, line) = extract_location(item, project_root);
253 let preview = read_line_preview(&project_root.join(&path), line);
254 let kind =
255 symbol_kind_name(item.get("kind").and_then(Value::as_u64).unwrap_or(0)).to_string();
256 return Ok(Some((
257 SymbolMatch {
258 path,
259 line,
260 kind,
261 preview,
262 body: None,
263 },
264 method.to_string(),
265 )));
266 }
267
268 debug!("find_go_receiver_method: no match found for '{name}'");
269 Ok(None)
270}
271
272pub async fn find_refs(
280 name: &str,
281 client: &mut LspClient,
282 file_tracker: &mut FileTracker,
283 project_root: &Path,
284) -> anyhow::Result<Vec<ReferenceMatch>> {
285 let symbols = find_symbol(name, client, project_root).await?;
289 debug!(
290 "find_refs: find_symbol('{name}') returned {} results",
291 symbols.len()
292 );
293 let (symbol, token) = if let Some(sym) = symbols.into_iter().next() {
294 debug!("find_refs: direct match at {}:{}", sym.path, sym.line);
295 let token = if Path::new(&sym.path).extension().and_then(|e| e.to_str()) == Some("go")
299 && name.contains('.')
300 {
301 name.rsplit('.').next().unwrap_or(name).to_string()
302 } else {
303 name.to_string()
304 };
305 (sym, token)
306 } else if name.contains('.') {
307 debug!("find_refs: no direct match, trying go receiver method path");
308 let result = find_go_receiver_method(name, client, project_root).await?;
309 debug!(
310 "find_refs: find_go_receiver_method returned: {}",
311 result.is_some()
312 );
313 result.with_context(|| format!("symbol '{name}' not found"))?
314 } else {
315 anyhow::bail!("symbol '{name}' not found");
316 };
317
318 let abs_path = project_root.join(&symbol.path);
320 let was_already_open = file_tracker.is_open(&abs_path);
321 file_tracker
322 .ensure_open(&abs_path, client.transport_mut())
323 .await?;
324 if !was_already_open {
325 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
327 }
328
329 let uri = crate::lsp::client::path_to_uri(&abs_path)?;
331 let (ref_line, ref_char) = find_name_position(&abs_path, symbol.line, &token);
332
333 let params = json!({
334 "textDocument": { "uri": uri.as_str() },
335 "position": { "line": ref_line, "character": ref_char },
336 "context": { "includeDeclaration": true }
337 });
338
339 let request_id = client
340 .transport_mut()
341 .send_request("textDocument/references", params)
342 .await?;
343
344 let response = client
345 .wait_for_response_public(request_id)
346 .await
347 .context("textDocument/references request failed")?;
348
349 Ok(parse_reference_results(
350 &response,
351 &symbol.path,
352 symbol.line,
353 project_root,
354 ))
355}
356
357#[derive(Debug, serde::Serialize)]
359pub struct ContainingSymbol {
360 pub name: String,
361 pub kind: String,
362 pub line: u32,
363}
364
365#[derive(Debug, serde::Serialize)]
367pub struct ReferenceMatch {
368 pub path: String,
369 pub line: u32,
370 pub preview: String,
371 pub is_definition: bool,
372 #[serde(skip_serializing_if = "Option::is_none")]
374 pub containing_symbol: Option<ContainingSymbol>,
375}
376
377#[must_use]
380pub fn find_innermost_containing(
381 symbols: &[crate::commands::list::SymbolEntry],
382 line: u32,
383) -> Option<ContainingSymbol> {
384 for sym in symbols {
385 if sym.line <= line && line <= sym.end_line {
386 if !sym.children.is_empty() {
388 if let Some(child) = find_innermost_containing(&sym.children, line) {
389 return Some(child);
390 }
391 }
392 return Some(ContainingSymbol {
393 name: sym.name.clone(),
394 kind: sym.kind.clone(),
395 line: sym.line,
396 });
397 }
398 }
399 None
400}
401
402fn parse_symbol_results(response: &Value, query: &str, project_root: &Path) -> Vec<SymbolMatch> {
403 let Some(items) = response.as_array() else {
404 return Vec::new();
405 };
406
407 let mut results = Vec::new();
408 for item in items {
409 let name = item.get("name").and_then(Value::as_str).unwrap_or_default();
410
411 let match_name = lang_go::base_name(name);
416 if !match_name.eq_ignore_ascii_case(query) && !match_name.starts_with(query) {
417 continue;
418 }
419
420 let kind = symbol_kind_name(item.get("kind").and_then(Value::as_u64).unwrap_or(0));
421
422 let (path, line) = extract_location(item, project_root);
423 let preview = read_line_preview(&project_root.join(&path), line);
424
425 results.push(SymbolMatch {
426 path,
427 line,
428 kind: kind.to_string(),
429 preview,
430 body: None,
431 });
432 }
433
434 results.sort_by(|a, b| a.path.cmp(&b.path).then(a.line.cmp(&b.line)));
435 results
436}
437
438const EXCLUDED_DIRS: &[&str] = &[
440 "target/",
441 ".git/",
442 "node_modules/",
443 ".mypy_cache/",
444 "__pycache__/",
445 ".cache/",
446 "dist/",
447 "build/",
448 ".next/",
449 ".nuxt/",
450];
451
452fn parse_reference_results(
453 response: &Value,
454 def_path: &str,
455 def_line: u32,
456 project_root: &Path,
457) -> Vec<ReferenceMatch> {
458 let Some(locations) = response.as_array() else {
459 return Vec::new();
460 };
461
462 let mut results = Vec::new();
463 for loc in locations {
464 let uri = loc.get("uri").and_then(Value::as_str).unwrap_or_default();
465
466 #[allow(clippy::cast_possible_truncation)]
467 let line = loc
468 .pointer("/range/start/line")
469 .and_then(Value::as_u64)
470 .unwrap_or(0) as u32
471 + 1; let path = uri_to_relative_path(uri, project_root);
474
475 if EXCLUDED_DIRS
477 .iter()
478 .any(|dir| path.starts_with(dir) || path.contains(&format!("/{dir}")))
479 {
480 continue;
481 }
482
483 let abs_path = project_root.join(&path);
484 let preview = read_line_preview(&abs_path, line);
485 let is_definition = path == def_path && line == def_line;
486
487 results.push(ReferenceMatch {
488 path,
489 line,
490 preview,
491 is_definition,
492 containing_symbol: None,
493 });
494 }
495
496 results.sort_by(|a, b| {
498 b.is_definition
499 .cmp(&a.is_definition)
500 .then(a.path.cmp(&b.path))
501 .then(a.line.cmp(&b.line))
502 });
503
504 results
505}
506
507fn extract_location(item: &Value, project_root: &Path) -> (String, u32) {
508 let uri = item
509 .pointer("/location/uri")
510 .and_then(Value::as_str)
511 .unwrap_or_default();
512
513 #[allow(clippy::cast_possible_truncation)]
514 let line = item
515 .pointer("/location/range/start/line")
516 .and_then(Value::as_u64)
517 .unwrap_or(0) as u32
518 + 1; (uri_to_relative_path(uri, project_root), line)
521}
522
523fn uri_to_relative_path(uri: &str, project_root: &Path) -> String {
524 let path = uri.strip_prefix("file://").unwrap_or(uri);
525 let abs = Path::new(path);
526 abs.strip_prefix(project_root)
527 .unwrap_or(abs)
528 .to_string_lossy()
529 .to_string()
530}
531
532#[allow(clippy::cast_possible_truncation)]
538fn find_name_position(path: &Path, line: u32, name: &str) -> (u32, u32) {
539 let Some(content) = std::fs::read_to_string(path).ok() else {
540 return (line.saturating_sub(1), 0);
541 };
542
543 let lines: Vec<&str> = content.lines().collect();
544 let start = line.saturating_sub(1) as usize;
545
546 for offset in 0..4 {
548 let idx = start + offset;
549 if idx >= lines.len() {
550 break;
551 }
552 if let Some(col) = lines[idx].find(name) {
553 return (idx as u32, col as u32);
554 }
555 }
556
557 (line.saturating_sub(1), 0)
558}
559
560fn read_line_preview(path: &Path, line: u32) -> String {
561 std::fs::read_to_string(path)
562 .ok()
563 .and_then(|content| {
564 content
565 .lines()
566 .nth(line.saturating_sub(1) as usize)
567 .map(|l| l.trim().to_string())
568 })
569 .unwrap_or_default()
570}
571
572#[must_use]
578pub fn extract_symbol_body(path: &Path, start_line: u32) -> Option<String> {
579 let content = std::fs::read_to_string(path).ok()?;
580 let lines: Vec<&str> = content.lines().collect();
581 let start = start_line.saturating_sub(1) as usize;
582 if start >= lines.len() {
583 return None;
584 }
585
586 let mut depth: i32 = 0;
587 let mut found_open = false;
588 let mut end = start;
589
590 for (i, line) in lines[start..].iter().enumerate() {
591 let idx = start + i;
592 for ch in line.chars() {
593 match ch {
594 '{' => {
595 depth += 1;
596 found_open = true;
597 }
598 '}' => {
599 depth -= 1;
600 }
601 _ => {}
602 }
603 }
604 end = idx;
605
606 if !found_open && line.trim_end().ends_with(';') {
608 break;
609 }
610 if found_open && depth <= 0 {
611 break;
612 }
613 if i >= 199 {
614 break;
615 }
616 }
617
618 let body: Vec<&str> = lines[start..=end].to_vec();
619 Some(body.join("\n"))
620}
621
622pub async fn find_impl(
630 name: &str,
631 lsp_client: &mut LspClient,
632 file_tracker: &mut FileTracker,
633 project_root: &Path,
634) -> anyhow::Result<Vec<SymbolMatch>> {
635 let symbols = find_symbol(name, lsp_client, project_root).await?;
637 let symbol = symbols
638 .first()
639 .with_context(|| format!("symbol '{name}' not found"))?;
640
641 let abs_path = project_root.join(&symbol.path);
643 let was_open = file_tracker.is_open(&abs_path);
644 file_tracker
645 .ensure_open(&abs_path, lsp_client.transport_mut())
646 .await?;
647 if !was_open {
648 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
649 }
650
651 let (line_0, char_0) = find_name_position(&abs_path, symbol.line, name);
653 let uri = client::path_to_uri(&abs_path)?;
654
655 let params = json!({
657 "textDocument": { "uri": uri.as_str() },
658 "position": { "line": line_0, "character": char_0 }
659 });
660
661 let request_id = lsp_client
662 .transport_mut()
663 .send_request("textDocument/implementation", params)
664 .await?;
665
666 let response = lsp_client
667 .wait_for_response_public(request_id)
668 .await
669 .context("textDocument/implementation request failed")?;
670
671 let results = parse_impl_results(&response, project_root);
672 if !results.is_empty() {
673 return Ok(results);
674 }
675
676 find_impl_via_refs(name, symbol, lsp_client, file_tracker, project_root).await
680}
681
682async fn find_impl_via_refs(
688 name: &str,
689 interface_symbol: &SymbolMatch,
690 client: &mut LspClient,
691 file_tracker: &mut FileTracker,
692 project_root: &Path,
693) -> anyhow::Result<Vec<SymbolMatch>> {
694 let refs = find_refs(name, client, file_tracker, project_root).await?;
695
696 let results: Vec<SymbolMatch> = refs
697 .into_iter()
698 .filter(|r| {
699 if r.is_definition {
701 return false;
702 }
703 let trimmed = r.preview.trim_start();
704 trimmed.starts_with("func ")
707 || trimmed.starts_with("function ")
708 || trimmed.starts_with("async function ")
709 || (trimmed.contains(name) && trimmed.ends_with('{'))
710 })
711 .filter(|r| {
712 !(r.path == interface_symbol.path && r.line == interface_symbol.line)
714 })
715 .map(|r| SymbolMatch {
716 path: r.path,
717 line: r.line,
718 kind: "implementation".to_string(),
719 preview: r.preview,
720 body: None,
721 })
722 .collect();
723
724 Ok(results)
725}
726
727fn parse_impl_results(response: &Value, project_root: &Path) -> Vec<SymbolMatch> {
728 let Some(items) = response.as_array() else {
730 return Vec::new();
731 };
732
733 let mut results = Vec::new();
734 for item in items {
735 let uri = item
738 .get("uri")
739 .or_else(|| item.get("targetUri"))
740 .and_then(Value::as_str)
741 .unwrap_or_default();
742
743 #[allow(clippy::cast_possible_truncation)]
744 let line = item
745 .pointer("/range/start/line")
746 .or_else(|| item.pointer("/targetRange/start/line"))
747 .and_then(Value::as_u64)
748 .unwrap_or(0) as u32
749 + 1; let path = uri_to_relative_path(uri, project_root);
752 let abs_path = project_root.join(&path);
753 let preview = read_line_preview(&abs_path, line);
754
755 results.push(SymbolMatch {
756 path,
757 line,
758 kind: "implementation".to_string(),
759 preview,
760 body: None,
761 });
762 }
763
764 results.sort_by(|a, b| a.path.cmp(&b.path).then(a.line.cmp(&b.line)));
765 results
766}
767
768#[must_use]
770pub fn symbol_kind_name(kind: u64) -> &'static str {
771 match kind {
772 1 => "file",
773 2 => "module",
774 3 => "namespace",
775 4 => "package",
776 5 => "class",
777 6 => "method",
778 7 => "property",
779 8 => "field",
780 9 => "constructor",
781 10 => "enum",
782 11 => "interface",
783 12 => "function",
784 13 => "variable",
785 14 => "constant",
786 15 => "string",
787 16 => "number",
788 17 => "boolean",
789 18 => "array",
790 19 => "object",
791 20 => "key",
792 21 => "null",
793 22 => "enum_member",
794 23 => "struct",
795 24 => "event",
796 25 => "operator",
797 26 => "type_parameter",
798 _ => "unknown",
799 }
800}
801
802#[cfg(test)]
803mod tests {
804 use super::*;
805
806 #[test]
807 fn symbol_kind_function() {
808 assert_eq!(symbol_kind_name(12), "function");
809 }
810
811 #[test]
812 fn symbol_kind_struct() {
813 assert_eq!(symbol_kind_name(23), "struct");
814 }
815
816 #[test]
817 fn uri_to_relative() {
818 let root = Path::new("/home/user/project");
819 let uri = "file:///home/user/project/src/lib.rs";
820 assert_eq!(uri_to_relative_path(uri, root), "src/lib.rs");
821 }
822
823 #[test]
824 fn uri_to_relative_outside_project() {
825 let root = Path::new("/home/user/project");
826 let uri = "file:///other/path/lib.rs";
827 assert_eq!(uri_to_relative_path(uri, root), "/other/path/lib.rs");
828 }
829
830 #[test]
831 fn parse_empty_symbol_results() {
832 let results = parse_symbol_results(&json!(null), "test", Path::new("/tmp"));
833 assert!(results.is_empty());
834 }
835
836 #[test]
837 fn parse_empty_reference_results() {
838 let results = parse_reference_results(&json!(null), "src/lib.rs", 1, Path::new("/tmp"));
839 assert!(results.is_empty());
840 }
841
842 #[test]
843 fn find_name_position_does_not_match_substring() {
844 let dir = tempfile::tempdir().unwrap();
845 let file = dir.path().join("test.ts");
846 std::fs::write(&file, "function renewed() {\n return new Thing();\n}").unwrap();
848
849 let (line, col) = find_name_position(&file, 1, "new");
851 assert!(line < 3, "line should be within search window");
856 let _ = col; }
858
859 #[test]
860 fn classify_definition_recognises_const() {
861 assert_eq!(
862 classify_definition(
863 "export const createPromotionsStep = createStep(",
864 "createPromotionsStep"
865 ),
866 Some("constant")
867 );
868 assert_eq!(
869 classify_definition("const foo = 1;", "foo"),
870 Some("constant")
871 );
872 }
873
874 #[test]
875 fn classify_definition_recognises_function() {
876 assert_eq!(
877 classify_definition("function greet(name: string) {", "greet"),
878 Some("function")
879 );
880 assert_eq!(
881 classify_definition("export function handleRequest(req) {", "handleRequest"),
882 Some("function")
883 );
884 assert_eq!(
885 classify_definition("pub fn run() -> Result<()> {", "run"),
886 Some("function")
887 );
888 }
889
890 #[test]
891 fn classify_definition_rejects_call_sites() {
892 assert_eq!(
893 classify_definition(
894 "const result = createPromotionsStep(data)",
895 "createPromotionsStep"
896 ),
897 None
898 );
899 assert_eq!(
900 classify_definition(
901 "import { createPromotionsStep } from '../steps'",
902 "createPromotionsStep"
903 ),
904 None
905 );
906 assert_eq!(
907 classify_definition("return createPromotionsStep(data)", "createPromotionsStep"),
908 None
909 );
910 }
911
912 #[test]
913 fn text_search_find_symbol_finds_const_export() {
914 use std::fs;
915 use tempfile::tempdir;
916
917 let dir = tempdir().unwrap();
918 fs::write(
919 dir.path().join("step.ts"),
920 "export const createPromotionsStep = createStep(\n stepId,\n async () => {}\n);\n",
921 )
922 .unwrap();
923 fs::write(
924 dir.path().join("workflow.ts"),
925 "import { createPromotionsStep } from './step';\nconst result = createPromotionsStep(data);\n",
926 ).unwrap();
927
928 let results = text_search_find_symbol("createPromotionsStep", dir.path());
929 assert_eq!(results.len(), 1);
931 assert!(results[0].path.ends_with("step.ts"));
932 assert_eq!(results[0].line, 1);
933 assert_eq!(results[0].kind, "constant");
934 }
935
936 #[test]
937 fn text_search_find_refs_returns_all_occurrences() {
938 use std::fs;
939 use tempfile::tempdir;
940
941 let dir = tempdir().unwrap();
942 fs::write(
943 dir.path().join("step.ts"),
944 "export const createPromotionsStep = createStep(stepId, async () => {});\n",
945 )
946 .unwrap();
947 fs::write(
948 dir.path().join("workflow.ts"),
949 "import { createPromotionsStep } from './step';\nconst out = createPromotionsStep(data);\n",
950 ).unwrap();
951
952 let results = text_search_find_refs("createPromotionsStep", dir.path());
953 assert_eq!(results.len(), 3);
954 assert!(results[0].is_definition);
956 }
957
958 #[test]
959 fn classify_definition_detects_kinds() {
960 assert_eq!(
961 classify_definition("export class MyClass {", "MyClass"),
962 Some("class")
963 );
964 assert_eq!(
965 classify_definition("export function doThing() {", "doThing"),
966 Some("function")
967 );
968 assert_eq!(
969 classify_definition("pub fn run() -> Result<()> {", "run"),
970 Some("function")
971 );
972 assert_eq!(
973 classify_definition("export const MY_CONST = 42", "MY_CONST"),
974 Some("constant")
975 );
976 assert_eq!(
977 classify_definition("export interface IService {", "IService"),
978 Some("interface")
979 );
980 assert_eq!(
981 classify_definition("pub struct Config {", "Config"),
982 Some("struct")
983 );
984 assert_eq!(
985 classify_definition("type MyAlias = string;", "MyAlias"),
986 Some("type_alias")
987 );
988 }
989
990 #[test]
991 fn command_to_request_find_symbol() {
992 use crate::cli::{Command, FindCommand};
993 use crate::client::command_to_request;
994 use crate::protocol::Request;
995
996 let cmd = Command::Find(FindCommand::Symbol {
997 name: "MyStruct".into(),
998 path: None,
999 src_only: false,
1000 include_body: false,
1001 });
1002 let req = command_to_request(&cmd);
1003 assert!(matches!(req, Request::FindSymbol { name, .. } if name == "MyStruct"));
1004 }
1005
1006 #[test]
1007 fn command_to_request_find_refs() {
1008 use crate::cli::{Command, FindCommand};
1009 use crate::client::command_to_request;
1010 use crate::protocol::Request;
1011
1012 let cmd = Command::Find(FindCommand::Refs {
1013 name: "my_func".into(),
1014 with_symbol: false,
1015 });
1016 let req = command_to_request(&cmd);
1017 assert!(matches!(req, Request::FindRefs { name, .. } if name == "my_func"));
1018 }
1019}