use crate::server::helpers::{
format_degraded_notice, millis_to_u64, parse_semantic_path, pathfinder_to_error_data,
require_symbol_target, serialize_metadata, treesitter_error_to_error_data,
};
use crate::server::types::{GetDefinitionParams, GetDefinitionResponse};
use crate::server::PathfinderServer;
use pathfinder_common::error::PathfinderError;
use pathfinder_common::types::DegradedReason;
use pathfinder_lsp::LspError;
use pathfinder_treesitter::symbols::did_you_mean;
use rmcp::model::ErrorData;
use super::definition_patterns;
impl PathfinderServer {
#[expect(
clippy::too_many_lines,
reason = "Sequential pipeline: parse → sandbox → TS → LSP (with warmup retry) → grep fallback. Extraction done at helper level; remaining orchestration is linear."
)]
pub(crate) async fn get_definition_impl(
&self,
params: GetDefinitionParams,
) -> Result<rmcp::model::CallToolResult, ErrorData> {
let start = std::time::Instant::now();
tracing::info!(
tool = "get_definition",
semantic_path = %params.semantic_path,
"get_definition: start"
);
let semantic_path = parse_semantic_path(¶ms.semantic_path)?;
require_symbol_target(&semantic_path, ¶ms.semantic_path)?;
if let Err(e) = self.sandbox.check(&semantic_path.file_path) {
let duration_ms = start.elapsed().as_millis();
tracing::warn!(
tool = "get_definition",
error_code = e.error_code(),
duration_ms,
"sandbox check failed"
);
return Err(pathfinder_to_error_data(&e));
}
let abs_file = self.workspace_root.path().join(&semantic_path.file_path);
if !abs_file.exists() {
let err = pathfinder_common::error::PathfinderError::FileNotFound {
path: abs_file.clone(),
};
tracing::warn!(
tool = "get_definition",
path = %abs_file.display(),
"file not found"
);
return Err(pathfinder_to_error_data(&err));
}
let ts_start = std::time::Instant::now();
let symbol_scope = self
.surgeon
.read_symbol_scope(self.workspace_root.path(), &semantic_path)
.await
.map_err(treesitter_error_to_error_data)?;
let tree_sitter_ms = ts_start.elapsed().as_millis();
let file_path = self.workspace_root.path().join(&semantic_path.file_path);
let file_content = match tokio::fs::read_to_string(&file_path).await {
Ok(content) => content,
Err(e) => {
tracing::warn!(
tool = "get_definition",
path = %file_path.display(),
error = %e,
"file read failed — LSP will receive empty content, goto_definition may return null"
);
String::new()
}
};
let _doc_guard = match self
.lawyer
.open_document(
self.workspace_root.path(),
&semantic_path.file_path,
&file_content,
)
.await
{
Ok(guard) => Some(guard),
Err(e) => {
tracing::warn!(
tool = "get_definition",
semantic_path = %semantic_path,
error = %e,
"open_document failed — LSP queries may return degraded results"
);
None
}
};
let lsp_start = std::time::Instant::now();
let lsp_result = self
.lawyer
.goto_definition(
self.workspace_root.path(),
&semantic_path.file_path,
u32::try_from(symbol_scope.start_line + 1).unwrap_or(1),
u32::try_from(symbol_scope.name_column + 1).unwrap_or(1),
)
.await;
let lsp_ms = lsp_start.elapsed().as_millis();
let duration_ms = start.elapsed().as_millis();
match lsp_result {
Ok(Some(def)) => {
tracing::info!(
tool = "get_definition",
file = %def.file,
definition_line = def.line,
tree_sitter_ms,
lsp_ms,
duration_ms,
engines_used = ?["tree-sitter", "lsp"],
"get_definition: complete"
);
Ok(Self::get_def_to_call_result(&GetDefinitionResponse {
file: def.file,
line: def.line,
column: def.column,
preview: def.preview,
degraded: false,
degraded_reason: None,
actionable_guidance: None,
lsp_readiness: Some("ready".to_owned()),
warm_start_in_progress: Some(false),
duration_ms: Some(millis_to_u64(duration_ms)),
resolution_strategy: Some("lsp".to_owned()),
}))
}
Ok(None) => {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
let retry_lsp_result = self
.lawyer
.goto_definition(
self.workspace_root.path(),
&semantic_path.file_path,
u32::try_from(symbol_scope.start_line + 1).unwrap_or(1),
u32::try_from(symbol_scope.name_column + 1).unwrap_or(1),
)
.await;
if let Ok(Some(def)) = retry_lsp_result {
tracing::info!(
tool = "get_definition",
file = %def.file,
definition_line = def.line,
tree_sitter_ms,
lsp_ms,
duration_ms = start.elapsed().as_millis(),
engines_used = ?["tree-sitter", "lsp"],
"get_definition: complete (succeeded on retry after warmup wait)"
);
return Ok(Self::get_def_to_call_result(&GetDefinitionResponse {
file: def.file,
line: def.line,
column: def.column,
preview: def.preview,
degraded: false,
degraded_reason: None,
actionable_guidance: None,
lsp_readiness: Some("warming_up".to_owned()),
warm_start_in_progress: Some(true),
duration_ms: Some(millis_to_u64(start.elapsed().as_millis())),
resolution_strategy: Some("lsp_retry".to_owned()),
}));
}
let duration_ms = start.elapsed().as_millis();
tracing::info!(
tool = "get_definition",
semantic_path = %params.semantic_path,
tree_sitter_ms,
lsp_ms,
duration_ms,
"get_definition: no definition found via LSP — attempting grep-based fallback"
);
if let Some(mut def) = self.fallback_definition_grep(&semantic_path).await {
if !matches!(
def.degraded_reason,
Some(DegradedReason::GrepFallbackImplScoped)
) {
def.degraded_reason = Some(DegradedReason::LspWarmupGrepFallback);
}
def.duration_ms = Some(millis_to_u64(start.elapsed().as_millis()));
let duration_ms = def.duration_ms.unwrap_or(0);
tracing::info!(
tool = "get_definition",
file = %def.file,
line = def.line,
duration_ms,
degraded = true,
degraded_reason = "lsp_warmup_grep_fallback",
engines_used = ?["tree-sitter", "lsp", "ripgrep"],
"get_definition: degraded complete (grep fallback after LSP None)"
);
return Ok(Self::get_def_to_call_result(&def));
}
tracing::info!(
tool = "get_definition",
semantic_path = %params.semantic_path,
tree_sitter_ms,
lsp_ms,
duration_ms,
"get_definition: no definition found (LSP None, grep empty)"
);
let did_you_mean_suggestions = self.compute_did_you_mean(&semantic_path).await;
let retry_after = if self.lawyer.is_warm_start_complete() {
None
} else {
Some(10u32) };
Err(pathfinder_to_error_data(&PathfinderError::SymbolNotFound {
semantic_path: params.semantic_path,
did_you_mean: did_you_mean_suggestions,
retry_after_seconds: retry_after,
}))
}
Err(LspError::NoLspAvailable) => {
tracing::info!(
tool = "get_definition",
symbol = %semantic_path,
"get_definition: no LSP — attempting grep-based fallback"
);
if let Some(mut def) = self.fallback_definition_grep(&semantic_path).await {
if !matches!(
def.degraded_reason,
Some(DegradedReason::GrepFallbackImplScoped)
) {
def.degraded_reason = Some(DegradedReason::NoLspGrepFallback);
}
def.duration_ms = Some(millis_to_u64(start.elapsed().as_millis()));
let duration_ms = def.duration_ms.unwrap_or(0);
tracing::info!(
tool = "get_definition",
file = %def.file,
line = def.line,
duration_ms,
degraded = true,
degraded_reason = "no_lsp_grep_fallback",
engines_used = ?["tree-sitter", "ripgrep"],
"get_definition: degraded complete (grep fallback)"
);
return Ok(Self::get_def_to_call_result(&def));
}
let duration_ms = start.elapsed().as_millis();
tracing::info!(
tool = "get_definition",
duration_ms,
degraded = true,
degraded_reason = "no_lsp",
engines_used = ?["none"],
"get_definition: degraded (no LSP, grep fallback also empty)"
);
Err(pathfinder_to_error_data(&PathfinderError::NoLspAvailable {
language: symbol_scope.language,
}))
}
Err(LspError::Timeout { .. }) => {
tracing::info!(
tool = "get_definition",
semantic_path = %params.semantic_path,
"get_definition: LSP timed out — attempting grep-based fallback"
);
if let Some(mut def) = self.fallback_definition_grep(&semantic_path).await {
if !matches!(
def.degraded_reason,
Some(DegradedReason::GrepFallbackImplScoped)
) {
def.degraded_reason = Some(DegradedReason::LspTimeoutGrepFallback);
}
def.duration_ms = Some(millis_to_u64(start.elapsed().as_millis()));
let duration_ms = def.duration_ms.unwrap_or(0);
tracing::info!(
tool = "get_definition",
file = %def.file,
line = def.line,
duration_ms,
degraded = true,
degraded_reason = "lsp_timeout_grep_fallback",
engines_used = ?["tree-sitter", "lsp", "ripgrep"],
"get_definition: degraded complete (grep fallback after timeout)"
);
return Ok(Self::get_def_to_call_result(&def));
}
tracing::warn!(
tool = "get_definition",
semantic_path = %params.semantic_path,
"get_definition: LSP timed out and grep fallback found no match"
);
Err(pathfinder_to_error_data(&PathfinderError::LspError {
message: "LSP timed out and grep fallback found no match".to_owned(),
}))
}
Err(e) => {
tracing::warn!(
tool = "get_definition",
error = %e,
tree_sitter_ms,
lsp_ms,
duration_ms,
engines_used = ?["lsp"],
"get_definition: LSP error — attempting grep fallback"
);
if let Some(mut def) = self.fallback_definition_grep(&semantic_path).await {
if !matches!(
def.degraded_reason,
Some(DegradedReason::GrepFallbackImplScoped)
) {
def.degraded_reason = Some(DegradedReason::LspErrorGrepFallback);
}
def.degraded = true;
def.duration_ms = Some(millis_to_u64(start.elapsed().as_millis()));
let duration_ms = def.duration_ms.unwrap_or(0);
tracing::info!(
tool = "get_definition",
file = %def.file,
line = def.line,
duration_ms,
degraded = true,
degraded_reason = "lsp_error_grep_fallback",
engines_used = ?["tree-sitter", "lsp", "ripgrep"],
"get_definition: degraded complete (grep fallback after LSP error)"
);
return Ok(Self::get_def_to_call_result(&def));
}
let duration_ms = start.elapsed().as_millis();
tracing::warn!(
tool = "get_definition",
error = %e,
duration_ms,
"get_definition: LSP error and grep fallback found no match"
);
Err(pathfinder_to_error_data(&PathfinderError::LspError {
message: e.to_string(),
}))
}
}
}
fn get_def_to_call_result(def: &GetDefinitionResponse) -> rmcp::model::CallToolResult {
let text = if def.degraded {
let notice = def
.degraded_reason
.as_ref()
.map_or_else(|| "DEGRADED (unknown)".to_owned(), format_degraded_notice);
format!(
"{notice}\n{}:L{} — {}\n[completed in {}ms]",
def.file,
def.line,
if def.preview.is_empty() {
"(no preview)"
} else {
&def.preview
},
def.duration_ms.unwrap_or(0),
)
} else {
format!(
"{}:L{} col:{} — {}\n[completed in {}ms]",
def.file,
def.line,
def.column,
if def.preview.is_empty() {
"(no preview)"
} else {
&def.preview
},
def.duration_ms.unwrap_or(0),
)
};
let mut res = rmcp::model::CallToolResult::success(vec![rmcp::model::Content::text(text)]);
res.structured_content = serialize_metadata(def);
res
}
async fn compute_did_you_mean(
&self,
semantic_path: &pathfinder_common::types::SemanticPath,
) -> Vec<String> {
let Some(ref symbol_chain) = semantic_path.symbol_chain else {
return Vec::new();
};
let symbols = self
.surgeon
.extract_symbols(self.workspace_root.path(), &semantic_path.file_path)
.await;
let Ok(symbols) = symbols else {
return Vec::new();
};
did_you_mean(&symbols, symbol_chain, 3)
}
async fn fallback_definition_grep(
&self,
semantic_path: &pathfinder_common::types::SemanticPath,
) -> Option<GetDefinitionResponse> {
let symbol_chain = semantic_path.symbol_chain.as_ref()?;
let symbol_name = symbol_chain.segments.last()?.name.clone();
let expected_file = &semantic_path.file_path;
if let Some(result) = self
.grep_definition_in_file(symbol_name.clone(), expected_file.clone())
.await
{
return Some(result);
}
if symbol_chain.segments.len() >= 2 {
let parent_name = symbol_chain.segments[symbol_chain.segments.len() - 2]
.name
.clone();
let ext = expected_file
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
let path_glob = format!("**/*.{ext}");
if let Some(result) = self
.grep_impl_method(&parent_name, &symbol_name, &path_glob)
.await
{
return Some(result);
}
}
if let Some(result) = self.grep_definition_global(symbol_name.clone()).await {
return Some(result);
}
self.grep_symbol_broad(&symbol_name).await
}
async fn grep_definition_in_file(
&self,
symbol_name: String,
file_path: std::path::PathBuf,
) -> Option<GetDefinitionResponse> {
let ext = file_path.extension().and_then(|e| e.to_str()).unwrap_or("");
let patterns = definition_patterns(ext, &symbol_name);
let glob = file_path.to_string_lossy().replace('\\', "/");
for pattern in patterns {
let search_result = self
.scout
.search(&pathfinder_search::SearchParams {
workspace_root: self.workspace_root.path().to_path_buf(),
query: pattern,
is_regex: true,
max_results: 5,
path_glob: glob.clone(),
exclude_glob: String::default(),
context_lines: 0,
offset: 0,
})
.await;
if let Ok(result) = search_result {
if !result.matches.is_empty() {
let m = &result.matches[0];
return Some(GetDefinitionResponse {
file: m.file.clone(),
line: u32::try_from(m.line).unwrap_or(u32::MAX),
column: u32::try_from(m.column).unwrap_or(1),
preview: m.content.clone(),
degraded: true,
degraded_reason: Some(DegradedReason::GrepFallbackFileScoped),
actionable_guidance: Some(
DegradedReason::GrepFallbackFileScoped.guidance(),
),
lsp_readiness: Some("unavailable".to_owned()),
warm_start_in_progress: None,
duration_ms: None,
resolution_strategy: Some("grep_file".to_owned()),
});
}
} else if let Err(e) = search_result {
tracing::warn!(
tool = "get_definition",
strategy = "grep_definition_in_file",
error = %e,
"scout.search failed during grep fallback"
);
}
}
None
}
async fn grep_impl_method(
&self,
parent_name: &str,
method_name: &str,
path_glob: &str,
) -> Option<GetDefinitionResponse> {
let parent_escaped = regex::escape(parent_name);
let impl_pattern = format!(r"impl\s+(?:<[^>]+>\s+)?{parent_escaped}\b");
let search_result = self
.scout
.search(&pathfinder_search::SearchParams {
workspace_root: self.workspace_root.path().to_path_buf(),
query: impl_pattern,
is_regex: true,
max_results: 10,
path_glob: path_glob.to_owned(),
exclude_glob: String::default(),
context_lines: 0,
offset: 0,
})
.await;
if let Ok(result) = search_result {
for m in &result.matches {
let ext = std::path::Path::new(&m.file)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
let method_escaped = regex::escape(method_name);
let method_pattern = match ext {
"rs" => format!(
r"(?:(?:pub|crate)\s*(?:\([^)]*\)\s*)?(?:async\s*)?)?fn\s+{method_escaped}\b"
),
"ts" | "js" | "tsx" | "jsx" => format!(
r"(?:(?:export\s+(?:default\s*)?)?(?:async\s+)?)?(?:function\s+{method_escaped}\b|{method_escaped}\s*[=:])"
),
"py" => format!(r"(?:async\s+)?def\s+{method_escaped}\b"),
"go" => format!(r"func\s+(?:\([^)]*\)\s+)?{method_escaped}\b"),
"java" => format!(
r"(?:public\s+|private\s+|protected\s+|static\s+|final\s+|abstract\s+)*(?:<[^>]*>\s+)?[a-zA-Z_][a-zA-Z0-9_<>\[\],\s]+\s+{method_escaped}\s*\("
),
_ => format!(r"\b{method_escaped}\b"),
};
let file_search = self
.scout
.search(&pathfinder_search::SearchParams {
workspace_root: self.workspace_root.path().to_path_buf(),
query: method_pattern,
is_regex: true,
max_results: 5,
path_glob: m.file.clone(),
exclude_glob: String::default(),
context_lines: 0,
offset: 0,
})
.await;
if let Ok(file_result) = file_search {
if !file_result.matches.is_empty() {
let hit = &file_result.matches[0];
return Some(GetDefinitionResponse {
file: hit.file.clone(),
line: u32::try_from(hit.line).unwrap_or(u32::MAX),
column: u32::try_from(hit.column).unwrap_or(1),
preview: hit.content.clone(),
degraded: true,
degraded_reason: Some(DegradedReason::GrepFallbackImplScoped),
actionable_guidance: Some(
DegradedReason::GrepFallbackImplScoped.guidance(),
),
lsp_readiness: Some("unavailable".to_owned()),
warm_start_in_progress: None,
duration_ms: None,
resolution_strategy: Some("grep_impl".to_owned()),
});
}
} else if let Err(e) = file_search {
tracing::warn!(
tool = "get_definition",
strategy = "grep_impl_method",
error = %e,
"scout.search failed during impl-method grep fallback"
);
}
}
} else if let Err(e) = search_result {
tracing::warn!(
tool = "get_definition",
strategy = "grep_impl_block",
error = %e,
"scout.search failed during impl-block grep fallback"
);
}
None
}
async fn grep_definition_global(&self, symbol_name: String) -> Option<GetDefinitionResponse> {
let name = regex::escape(&symbol_name);
let pattern = format!(
r"(?:(?:pub|export|public|private|protected|internal|open)\s*(?:\([^)]*\)\s*)?(?:async\s*)?)?(?:fn|def|func|function|class|struct|type|interface|const|let|var|enum|trait|mod)\s+{name}\b"
);
let search_result = self
.scout
.search(&pathfinder_search::SearchParams {
workspace_root: self.workspace_root.path().to_path_buf(),
query: pattern,
is_regex: true,
max_results: 10,
path_glob: "**/*".to_owned(),
exclude_glob: "**/{test,tests,mock}*/**".to_owned(),
offset: 0,
context_lines: 0,
})
.await;
if let Ok(result) = search_result {
if !result.matches.is_empty() {
let m = &result.matches[0];
return Some(GetDefinitionResponse {
file: m.file.clone(),
line: u32::try_from(m.line).unwrap_or(u32::MAX),
column: u32::try_from(m.column).unwrap_or(1),
preview: m.content.clone(),
degraded: true,
degraded_reason: Some(DegradedReason::GrepFallbackGlobal),
actionable_guidance: Some(DegradedReason::GrepFallbackGlobal.guidance()),
lsp_readiness: Some("unavailable".to_owned()),
warm_start_in_progress: None,
duration_ms: None,
resolution_strategy: Some("grep_global".to_owned()),
});
}
} else if let Err(e) = search_result {
tracing::warn!(
tool = "get_definition",
strategy = "grep_definition_global",
error = %e,
"scout.search failed during global grep fallback"
);
}
None
}
async fn grep_symbol_broad(&self, symbol_name: &str) -> Option<GetDefinitionResponse> {
let name = regex::escape(symbol_name);
let pattern = format!(r"\b{name}\b");
let search_result = self
.scout
.search(&pathfinder_search::SearchParams {
workspace_root: self.workspace_root.path().to_path_buf(),
query: pattern,
is_regex: true,
max_results: 20,
path_glob: "**/*".to_owned(),
exclude_glob: "**/{test,tests,mock}*/**".to_owned(),
offset: 0,
context_lines: 0,
})
.await;
if let Ok(result) = search_result {
if !result.matches.is_empty() {
let m = &result.matches[0];
return Some(GetDefinitionResponse {
file: m.file.clone(),
line: u32::try_from(m.line).unwrap_or(u32::MAX),
column: u32::try_from(m.column).unwrap_or(1),
preview: m.content.clone(),
degraded: true,
degraded_reason: Some(DegradedReason::GrepFallbackGlobal),
actionable_guidance: Some(DegradedReason::GrepFallbackGlobal.guidance()),
lsp_readiness: Some("unavailable".to_owned()),
warm_start_in_progress: None,
duration_ms: None,
resolution_strategy: Some("grep_broad".to_owned()),
});
}
} else if let Err(e) = search_result {
tracing::warn!(
tool = "get_definition",
strategy = "grep_symbol_broad",
error = %e,
"scout.search failed during broad grep fallback"
);
}
None
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::super::test_helpers::{make_scope, make_server_with_lawyer, make_temp_workspace};
use super::*;
use crate::server::types::GetDefinitionParams;
use crate::server::PathfinderServer;
use pathfinder_common::config::PathfinderConfig;
use pathfinder_common::sandbox::Sandbox;
use pathfinder_common::types::{DegradedReason, WorkspaceRoot};
use pathfinder_lsp::{DefinitionLocation, MockLawyer};
use pathfinder_search::MockScout;
use pathfinder_treesitter::mock::MockSurgeon;
use std::sync::Arc;
fn unpack_def(res: rmcp::model::CallToolResult) -> crate::server::types::GetDefinitionResponse {
serde_json::from_value(res.structured_content.expect("structured_content")).unwrap()
}
#[tokio::test]
async fn test_get_definition_routes_to_lawyer_success() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let lawyer = Arc::new(MockLawyer::default());
lawyer.set_goto_definition_result(Ok(Some(DefinitionLocation {
file: "src/auth.rs".into(),
line: 42,
column: 5,
preview: "pub fn login() -> bool {".into(),
})));
let (server, _ws) = make_server_with_lawyer(surgeon, lawyer.clone());
let params = GetDefinitionParams {
semantic_path: "src/auth.rs::login".to_owned(),
};
let result = server.get_definition_impl(params).await;
let call_res = result.expect("should succeed");
let val = unpack_def(call_res);
assert_eq!(val.file, "src/auth.rs");
assert_eq!(val.line, 42);
assert_eq!(val.preview, "pub fn login() -> bool {");
assert!(!val.degraded);
assert_eq!(lawyer.goto_definition_call_count(), 1);
}
#[tokio::test]
async fn test_get_definition_degrades_when_no_lsp() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let lawyer = Arc::new(pathfinder_lsp::NoOpLawyer);
let ws_dir = make_temp_workspace();
let ws = WorkspaceRoot::new(ws_dir.path()).expect("valid root");
let config = PathfinderConfig::default();
let sandbox = Sandbox::new(ws.path(), &config.sandbox);
let server = PathfinderServer::with_all_engines(
ws,
config,
sandbox,
Arc::new(MockScout::default()),
surgeon,
lawyer,
);
let params = GetDefinitionParams {
semantic_path: "src/auth.rs::login".to_owned(),
};
let result = server.get_definition_impl(params).await;
let Err(err) = result else {
panic!("expected error but got Ok");
};
let code = err
.data
.as_ref()
.and_then(|d| d.get("error"))
.and_then(|v| v.as_str())
.unwrap_or("");
assert_eq!(code, "NO_LSP_AVAILABLE");
}
#[tokio::test]
async fn test_get_definition_rejects_empty_semantic_path() {
let surgeon = Arc::new(MockSurgeon::default());
let lawyer = Arc::new(MockLawyer::default());
let (server, _ws) = make_server_with_lawyer(surgeon, lawyer);
let params = GetDefinitionParams {
semantic_path: String::default(), };
let result = server.get_definition_impl(params).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_get_definition_rejects_sandbox_denied_path() {
let surgeon = Arc::new(MockSurgeon::new());
let lawyer = Arc::new(MockLawyer::default());
let (server, _ws) = make_server_with_lawyer(surgeon, lawyer);
let params = GetDefinitionParams {
semantic_path: ".git/objects/abc::def".to_owned(), };
let result = server.get_definition_impl(params).await;
let Err(err) = result else {
panic!("expected error but got Ok");
};
let code = err
.data
.as_ref()
.and_then(|d| d.get("error"))
.and_then(|v| v.as_str())
.unwrap_or("");
assert_eq!(code, "ACCESS_DENIED");
}
#[tokio::test]
async fn test_get_definition_lsp_error_no_grep_match_returns_lsp_error() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let lawyer = Arc::new(MockLawyer::default());
lawyer
.set_goto_definition_result(Err(LspError::Protocol("LSP protocol error".to_string())));
let (server, _ws) = make_server_with_lawyer(surgeon, lawyer);
let params = GetDefinitionParams {
semantic_path: "src/auth.rs::login".to_owned(),
};
let result = server.get_definition_impl(params).await;
let Err(err) = result else {
panic!("expected error but got Ok");
};
let code = err
.data
.as_ref()
.and_then(|d| d.get("error"))
.and_then(|v| v.as_str())
.unwrap_or("");
assert_eq!(code, "LSP_ERROR");
}
#[tokio::test]
async fn test_get_definition_generic_lsp_error_falls_back_to_grep() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let ws_dir = make_temp_workspace();
let ws = WorkspaceRoot::new(ws_dir.path()).expect("valid root");
let config = PathfinderConfig::default();
let sandbox = Sandbox::new(ws.path(), &config.sandbox);
std::fs::create_dir_all(ws_dir.path().join("src")).unwrap();
std::fs::write(
ws_dir.path().join("src/auth.rs"),
"fn login() -> bool { true }",
)
.unwrap();
let scout = Arc::new(MockScout::default());
scout.set_result(Ok(pathfinder_search::SearchResult {
matches: vec![pathfinder_search::SearchMatch {
file: "src/auth.rs".to_string(),
line: 1,
column: 1,
content: "fn login() -> bool { true }".to_string(),
context_before: vec![],
context_after: vec![],
enclosing_semantic_path: None,
is_definition: None,
version_hash: "sha256:abc".to_string(),
known: Some(false),
}],
total_matches: 1,
truncated: false,
files_searched: 0,
files_in_scope: 0,
binary_skipped: 0,
gitignored_skipped: 0,
other_skipped: 0,
}));
let lawyer = Arc::new(MockLawyer::default());
lawyer
.set_goto_definition_result(Err(LspError::Protocol("protocol violation".to_string())));
let server =
PathfinderServer::with_all_engines(ws, config, sandbox, scout, surgeon, lawyer);
let params = GetDefinitionParams {
semantic_path: "src/auth.rs::login".to_owned(),
};
let result = server.get_definition_impl(params).await;
let Ok(res) = result else {
panic!("expected Ok with grep fallback, got Err");
};
let val = unpack_def(res);
assert!(val.degraded, "should be degraded");
assert_eq!(val.file, "src/auth.rs");
assert_eq!(
val.degraded_reason,
Some(DegradedReason::LspErrorGrepFallback),
"degraded_reason should be lsp_error_grep_fallback: {:?}",
val.degraded_reason
);
}
#[tokio::test]
async fn test_get_definition_connection_lost_falls_back_to_grep() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let ws_dir = make_temp_workspace();
let ws = WorkspaceRoot::new(ws_dir.path()).expect("valid root");
let config = PathfinderConfig::default();
let sandbox = Sandbox::new(ws.path(), &config.sandbox);
std::fs::create_dir_all(ws_dir.path().join("src")).unwrap();
std::fs::write(
ws_dir.path().join("src/auth.rs"),
"fn login() -> bool { true }",
)
.unwrap();
let scout = Arc::new(MockScout::default());
scout.set_result(Ok(pathfinder_search::SearchResult {
matches: vec![pathfinder_search::SearchMatch {
file: "src/auth.rs".to_string(),
line: 1,
column: 1,
content: "fn login() -> bool { true }".to_string(),
context_before: vec![],
context_after: vec![],
enclosing_semantic_path: None,
is_definition: None,
version_hash: "sha256:abc".to_string(),
known: Some(false),
}],
total_matches: 1,
truncated: false,
files_searched: 0,
files_in_scope: 0,
binary_skipped: 0,
gitignored_skipped: 0,
other_skipped: 0,
}));
let lawyer = Arc::new(MockLawyer::default());
lawyer.set_goto_definition_result(Err(LspError::ConnectionLost));
let server =
PathfinderServer::with_all_engines(ws, config, sandbox, scout, surgeon, lawyer);
let params = GetDefinitionParams {
semantic_path: "src/auth.rs::login".to_owned(),
};
let result = server.get_definition_impl(params).await;
let Ok(res) = result else {
panic!("expected Ok with grep fallback, got Err");
};
let val = unpack_def(res);
assert!(val.degraded, "should be degraded");
assert_eq!(
val.degraded_reason,
Some(DegradedReason::LspErrorGrepFallback),
"degraded_reason: {:?}",
val.degraded_reason
);
}
#[tokio::test]
async fn test_get_definition_lsp_none_no_grep_fallback_returns_symbol_not_found() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
surgeon
.extract_symbols_results
.lock()
.unwrap()
.push(Ok(Vec::new()));
let lawyer = Arc::new(MockLawyer::default());
let (server, _ws) = make_server_with_lawyer(surgeon, lawyer);
let params = GetDefinitionParams {
semantic_path: "src/auth.rs::login".to_owned(),
};
let result = server.get_definition_impl(params).await;
let Err(err) = result else {
panic!("expected error but got Ok");
};
let code = err
.data
.as_ref()
.and_then(|d| d.get("error"))
.and_then(|v| v.as_str())
.unwrap_or("");
assert_eq!(code, "SYMBOL_NOT_FOUND");
}
#[tokio::test]
async fn test_get_definition_grep_fallback_with_mock_scout() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let _lawyer = Arc::new(MockLawyer::default());
let ws_dir = make_temp_workspace();
let ws = WorkspaceRoot::new(ws_dir.path()).expect("valid root");
let config = PathfinderConfig::default();
let sandbox = Sandbox::new(ws.path(), &config.sandbox);
std::fs::create_dir_all(ws_dir.path().join("src")).unwrap();
std::fs::write(
ws_dir.path().join("src/other.rs"),
"fn login() -> bool { true }",
)
.unwrap();
let scout = Arc::new(MockScout::default());
scout.set_result(Ok(pathfinder_search::SearchResult {
matches: vec![pathfinder_search::SearchMatch {
file: "src/other.rs".to_string(),
line: 1,
column: 1,
content: "fn login() -> bool { true }".to_string(),
context_before: vec![],
context_after: vec![],
enclosing_semantic_path: None,
is_definition: None,
version_hash: "sha256:abc".to_string(),
known: Some(false),
}],
total_matches: 1,
truncated: false,
files_searched: 0,
files_in_scope: 0,
binary_skipped: 0,
gitignored_skipped: 0,
other_skipped: 0,
}));
let server = PathfinderServer::with_all_engines(
ws,
config,
sandbox,
scout,
surgeon,
Arc::new(pathfinder_lsp::NoOpLawyer),
);
let params = GetDefinitionParams {
semantic_path: "src/auth.rs::login".to_owned(),
};
let result = server.get_definition_impl(params).await;
let Ok(res) = result else {
panic!("expected Ok with grep fallback, got Err");
};
let val = unpack_def(res);
assert!(val.degraded);
assert_eq!(val.file, "src/other.rs");
assert!(val
.degraded_reason
.as_ref()
.unwrap()
.to_string()
.contains("grep_fallback"));
}
#[tokio::test]
async fn test_get_definition_closes_document_on_success() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let lawyer = Arc::new(MockLawyer::default());
lawyer.set_goto_definition_result(Ok(Some(DefinitionLocation {
file: "src/auth.rs".into(),
line: 42,
column: 5,
preview: "pub fn login() -> bool {".into(),
})));
let (server, _ws) = make_server_with_lawyer(surgeon, lawyer.clone());
let params = GetDefinitionParams {
semantic_path: "src/auth.rs::login".to_owned(),
};
let _ = server.get_definition_impl(params).await;
tokio::task::yield_now().await;
assert_eq!(
lawyer.did_open_call_count(),
lawyer.did_close_call_count(),
"DS-1: did_open and did_close must be symmetric on success"
);
}
#[tokio::test]
async fn test_get_definition_closes_document_on_lsp_error() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let lawyer = Arc::new(MockLawyer::default());
lawyer.set_goto_definition_result(Err(LspError::Protocol("LSP crashed".to_string())));
let (server, _ws) = make_server_with_lawyer(surgeon, lawyer.clone());
let params = GetDefinitionParams {
semantic_path: "src/auth.rs::login".to_owned(),
};
let _ = server.get_definition_impl(params).await;
tokio::task::yield_now().await;
assert_eq!(
lawyer.did_open_call_count(),
lawyer.did_close_call_count(),
"DS-1: did_close must be called even when LSP returns an error"
);
}
#[tokio::test]
async fn test_get_definition_returns_did_you_mean_suggestions_on_symbol_not_found() {
use pathfinder_treesitter::surgeon::{ExtractedSymbol, SymbolKind};
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let symbols = vec![
ExtractedSymbol {
name: "logIn".to_owned(),
semantic_path: "logIn".to_owned(),
kind: SymbolKind::Function,
byte_range: 0..5,
start_line: 0,
end_line: 0,
name_column: 0,
access_level: pathfinder_treesitter::surgeon::AccessLevel::Public,
children: vec![],
},
ExtractedSymbol {
name: "logon".to_owned(),
semantic_path: "logon".to_owned(),
kind: SymbolKind::Function,
byte_range: 10..15,
start_line: 1,
end_line: 1,
name_column: 0,
access_level: pathfinder_treesitter::surgeon::AccessLevel::Public,
children: vec![],
},
];
surgeon
.extract_symbols_results
.lock()
.unwrap()
.push(Ok(symbols));
let lawyer = Arc::new(MockLawyer::default());
let (server, _ws) = make_server_with_lawyer(surgeon, lawyer);
let params = GetDefinitionParams {
semantic_path: "src/auth.rs::login".to_owned(),
};
let result = server.get_definition_impl(params).await;
let Err(err) = result else {
panic!("expected SYMBOL_NOT_FOUND error, got Ok");
};
let code = err
.data
.as_ref()
.and_then(|d| d.get("error"))
.and_then(|v| v.as_str())
.unwrap_or("");
assert_eq!(
code, "SYMBOL_NOT_FOUND",
"error code must be SYMBOL_NOT_FOUND"
);
let suggestions = err
.data
.as_ref()
.and_then(|d| d.get("details"))
.and_then(|d| d.get("did_you_mean"))
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
assert!(
!suggestions.is_empty(),
"did_you_mean must contain suggestions when similar symbols exist"
);
let has_login_variant = suggestions
.iter()
.any(|s| s.as_str().is_some_and(|s| s.contains("log")));
assert!(
has_login_variant,
"suggestions should include close matches like 'logIn' or 'logon', got: {suggestions:?}"
);
}
#[tokio::test]
async fn test_get_definition_grep_fallback_when_lsp_returns_none() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let lawyer = Arc::new(MockLawyer::default());
let scout = Arc::new(MockScout::default());
scout.set_result(Ok(pathfinder_search::SearchResult {
matches: vec![pathfinder_search::SearchMatch {
file: "src/auth.rs".to_owned(),
line: 10,
column: 4,
content: "pub fn login() -> bool {".to_owned(),
context_before: vec![],
context_after: vec![],
enclosing_semantic_path: Some("src/auth.rs::login".to_owned()),
is_definition: Some(true),
version_hash: "hash".to_owned(),
known: None,
}],
total_matches: 1,
truncated: false,
files_searched: 1,
files_in_scope: 1,
binary_skipped: 0,
gitignored_skipped: 0,
other_skipped: 0,
}));
let ws_dir = make_temp_workspace();
let ws = WorkspaceRoot::new(ws_dir.path()).expect("valid root");
let config = PathfinderConfig::default();
let sandbox = Sandbox::new(ws.path(), &config.sandbox);
let server =
PathfinderServer::with_all_engines(ws, config, sandbox, scout, surgeon, lawyer);
let params = GetDefinitionParams {
semantic_path: "src/auth.rs::login".to_owned(),
};
let result = server.get_definition_impl(params).await;
let call_res = result.expect("should succeed via grep fallback");
let val = unpack_def(call_res);
assert_eq!(val.file, "src/auth.rs");
assert_eq!(val.line, 10);
assert!(val.degraded, "should be degraded when using grep fallback");
assert!(val.degraded_reason.is_some(), "degraded_reason must be set");
}
#[tokio::test]
async fn test_get_definition_grep_fallback_when_no_lsp() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let lawyer = Arc::new(pathfinder_lsp::NoOpLawyer);
let scout = Arc::new(MockScout::default());
scout.set_result(Ok(pathfinder_search::SearchResult {
matches: vec![pathfinder_search::SearchMatch {
file: "src/auth.rs".to_owned(),
line: 10,
column: 4,
content: "pub fn login() -> bool {".to_owned(),
context_before: vec![],
context_after: vec![],
enclosing_semantic_path: Some("src/auth.rs::login".to_owned()),
is_definition: Some(true),
version_hash: "hash".to_owned(),
known: None,
}],
total_matches: 1,
truncated: false,
files_searched: 1,
files_in_scope: 1,
binary_skipped: 0,
gitignored_skipped: 0,
other_skipped: 0,
}));
let ws_dir = make_temp_workspace();
let ws = WorkspaceRoot::new(ws_dir.path()).expect("valid root");
let config = PathfinderConfig::default();
let sandbox = Sandbox::new(ws.path(), &config.sandbox);
let server =
PathfinderServer::with_all_engines(ws, config, sandbox, scout, surgeon, lawyer);
let params = GetDefinitionParams {
semantic_path: "src/auth.rs::login".to_owned(),
};
let result = server.get_definition_impl(params).await;
let call_res = result.expect("should succeed via grep fallback");
let val = unpack_def(call_res);
assert_eq!(val.file, "src/auth.rs");
assert_eq!(val.line, 10);
assert!(val.degraded, "should be degraded when using grep fallback");
}
#[tokio::test]
async fn test_get_definition_lsp_timeout_falls_back_to_grep() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let ws_dir = make_temp_workspace();
let ws = WorkspaceRoot::new(ws_dir.path()).expect("valid root");
let config = PathfinderConfig::default();
let sandbox = Sandbox::new(ws.path(), &config.sandbox);
std::fs::create_dir_all(ws_dir.path().join("src")).unwrap();
std::fs::write(
ws_dir.path().join("src/auth.rs"),
"pub fn login() -> bool { true }",
)
.unwrap();
let scout = Arc::new(MockScout::default());
scout.set_result(Ok(pathfinder_search::SearchResult {
matches: vec![pathfinder_search::SearchMatch {
file: "src/auth.rs".to_string(),
line: 1,
column: 1,
content: "pub fn login() -> bool { true }".to_string(),
context_before: vec![],
context_after: vec![],
enclosing_semantic_path: None,
is_definition: None,
version_hash: "sha256:abc".to_string(),
known: Some(false),
}],
total_matches: 1,
truncated: false,
files_searched: 0,
files_in_scope: 0,
binary_skipped: 0,
gitignored_skipped: 0,
other_skipped: 0,
}));
let lawyer = Arc::new(MockLawyer::default());
lawyer.set_goto_definition_result(Err(LspError::Timeout {
operation: "goto_definition".to_string(),
timeout_ms: 10000,
}));
let server =
PathfinderServer::with_all_engines(ws, config, sandbox, scout, surgeon, lawyer);
let params = GetDefinitionParams {
semantic_path: "src/auth.rs::login".to_owned(),
};
let result = server.get_definition_impl(params).await;
let Ok(res) = result else {
panic!("expected Ok with grep fallback after timeout, got Err");
};
let val = unpack_def(res);
assert!(val.degraded, "should be degraded");
assert_eq!(val.file, "src/auth.rs");
assert_eq!(
val.degraded_reason,
Some(DegradedReason::LspTimeoutGrepFallback),
"degraded_reason should be LspTimeoutGrepFallback: {:?}",
val.degraded_reason
);
}
#[tokio::test]
async fn test_get_definition_multi_strategy_fallback() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let ws_dir = make_temp_workspace();
let ws = WorkspaceRoot::new(ws_dir.path()).expect("valid root");
let config = PathfinderConfig::default();
let sandbox = Sandbox::new(ws.path(), &config.sandbox);
std::fs::create_dir_all(ws_dir.path().join("src")).unwrap();
std::fs::write(
ws_dir.path().join("src/other.rs"),
"pub fn login() -> bool { true }",
)
.unwrap();
let scout = Arc::new(MockScout::default());
scout.set_results(vec![
Ok(pathfinder_search::SearchResult {
matches: vec![],
total_matches: 0,
truncated: false,
files_searched: 1,
files_in_scope: 1,
binary_skipped: 0,
gitignored_skipped: 0,
other_skipped: 0,
}),
Ok(pathfinder_search::SearchResult {
matches: vec![pathfinder_search::SearchMatch {
file: "src/other.rs".to_string(),
line: 1,
column: 1,
content: "pub fn login() -> bool { true }".to_string(),
context_before: vec![],
context_after: vec![],
enclosing_semantic_path: None,
is_definition: None,
version_hash: "sha256:abc".to_string(),
known: Some(false),
}],
total_matches: 1,
truncated: false,
files_searched: 1,
files_in_scope: 1,
binary_skipped: 0,
gitignored_skipped: 0,
other_skipped: 0,
}),
]);
let server = PathfinderServer::with_all_engines(
ws,
config,
sandbox,
scout,
surgeon,
Arc::new(pathfinder_lsp::NoOpLawyer),
);
let params = GetDefinitionParams {
semantic_path: "src/auth.rs::login".to_owned(),
};
let result = server.get_definition_impl(params).await;
let Ok(res) = result else {
panic!("expected Ok with multi-strategy grep fallback, got Err");
};
let val = unpack_def(res);
assert!(val.degraded, "should be degraded");
assert_eq!(val.file, "src/other.rs");
assert_eq!(
val.degraded_reason,
Some(DegradedReason::NoLspGrepFallback),
"degraded_reason: {:?}",
val.degraded_reason
);
}
#[tokio::test]
async fn test_get_definition_warmup_retry_success() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let lawyer = Arc::new(MockLawyer::default());
lawyer.push_goto_definition_result(Ok(None));
lawyer.set_goto_definition_result(Ok(Some(DefinitionLocation {
file: "src/auth.rs".into(),
line: 42,
column: 5,
preview: "pub fn login() -> bool {".into(),
})));
let (server, _ws) = make_server_with_lawyer(surgeon, lawyer);
let params = GetDefinitionParams {
semantic_path: "src/auth.rs::login".to_owned(),
};
let result = server.get_definition_impl(params).await;
let call_res = result.expect("should succeed on retry");
let val = unpack_def(call_res);
assert_eq!(val.file, "src/auth.rs");
assert_eq!(val.line, 42);
assert!(!val.degraded, "should NOT be degraded on retry success");
assert_eq!(
val.resolution_strategy,
Some("lsp_retry".to_owned()),
"should indicate retry strategy"
);
assert_eq!(
val.lsp_readiness,
Some("warming_up".to_owned()),
"should indicate warming_up"
);
assert_eq!(
val.warm_start_in_progress,
Some(true),
"should indicate warm_start_in_progress"
);
}
#[tokio::test]
async fn test_get_definition_grep_fallback_with_two_segment_symbol() {
let surgeon = Arc::new(MockSurgeon::new());
let mut scope = make_scope();
scope.content = "pub fn my_method(&self) { ... }".to_string();
surgeon.read_symbol_scope_results.lock().unwrap().clear();
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(scope));
let scout = Arc::new(MockScout::default());
scout.set_result(Ok(pathfinder_search::SearchResult {
matches: vec![pathfinder_search::SearchMatch {
file: "src/mystruct.rs".to_string(),
line: 10,
column: 4,
content: "pub fn my_method(&self) {}".to_string(),
context_before: vec![],
context_after: vec![],
enclosing_semantic_path: None,
is_definition: None,
version_hash: "sha256:def".to_string(),
known: Some(false),
}],
total_matches: 1,
truncated: false,
files_searched: 1,
files_in_scope: 1,
binary_skipped: 0,
gitignored_skipped: 0,
other_skipped: 0,
}));
let ws_dir = make_temp_workspace();
let ws = WorkspaceRoot::new(ws_dir.path()).expect("valid root");
let config = PathfinderConfig::default();
let sandbox = Sandbox::new(ws.path(), &config.sandbox);
std::fs::write(
ws_dir.path().join("src/mystruct.rs"),
"impl MyStruct { pub fn my_method(&self) {} }",
)
.unwrap();
let server = PathfinderServer::with_all_engines(
ws,
config,
sandbox,
scout,
surgeon,
Arc::new(pathfinder_lsp::NoOpLawyer),
);
let params = GetDefinitionParams {
semantic_path: "src/mystruct.rs::MyStruct.my_method".to_owned(),
};
let result = server.get_definition_impl(params).await;
match &result {
Ok(res) => {
let val = unpack_def(res.clone());
assert!(val.degraded, "should be degraded");
assert_eq!(val.file, "src/mystruct.rs");
assert_eq!(val.line, 10);
assert!(
val.degraded_reason.is_some(),
"degraded_reason should be set"
);
assert_eq!(
val.degraded_reason,
Some(DegradedReason::NoLspGrepFallback),
"degraded_reason: {:?}",
val.degraded_reason
);
}
Err(err) => {
let code = err
.data
.as_ref()
.and_then(|d| d.get("error"))
.and_then(|v| v.as_str())
.unwrap_or("");
panic!("expected Ok with grep fallback, got Err({code}): {err:?}");
}
}
}
#[tokio::test]
#[allow(clippy::too_many_lines)]
async fn test_get_definition_grep_impl_method_strategy() {
let surgeon = Arc::new(MockSurgeon::new());
surgeon
.read_symbol_scope_results
.lock()
.unwrap()
.push(Ok(make_scope()));
let ws_dir = make_temp_workspace();
let ws = WorkspaceRoot::new(ws_dir.path()).expect("valid root");
let config = PathfinderConfig::default();
let sandbox = Sandbox::new(ws.path(), &config.sandbox);
std::fs::create_dir_all(ws_dir.path().join("src")).unwrap();
std::fs::write(
ws_dir.path().join("src/sandbox.rs"),
"impl Sandbox {\n pub fn check(&self) -> bool { true }\n}",
)
.unwrap();
let scout = Arc::new(MockScout::default());
scout.set_results(vec![
Ok(pathfinder_search::SearchResult {
matches: vec![],
total_matches: 0,
truncated: false,
files_searched: 1,
files_in_scope: 1,
binary_skipped: 0,
gitignored_skipped: 0,
other_skipped: 0,
}),
Ok(pathfinder_search::SearchResult {
matches: vec![],
total_matches: 0,
truncated: false,
files_searched: 1,
files_in_scope: 1,
binary_skipped: 0,
gitignored_skipped: 0,
other_skipped: 0,
}),
Ok(pathfinder_search::SearchResult {
matches: vec![],
total_matches: 0,
truncated: false,
files_searched: 1,
files_in_scope: 1,
binary_skipped: 0,
gitignored_skipped: 0,
other_skipped: 0,
}),
Ok(pathfinder_search::SearchResult {
matches: vec![],
total_matches: 0,
truncated: false,
files_searched: 1,
files_in_scope: 1,
binary_skipped: 0,
gitignored_skipped: 0,
other_skipped: 0,
}),
Ok(pathfinder_search::SearchResult {
matches: vec![pathfinder_search::SearchMatch {
file: "src/sandbox.rs".to_string(),
line: 1,
column: 1,
content: "impl Sandbox {".to_string(),
context_before: vec![],
context_after: vec![],
enclosing_semantic_path: None,
is_definition: None,
version_hash: "sha256:abc".to_string(),
known: Some(false),
}],
total_matches: 1,
truncated: false,
files_searched: 1,
files_in_scope: 1,
binary_skipped: 0,
gitignored_skipped: 0,
other_skipped: 0,
}),
Ok(pathfinder_search::SearchResult {
matches: vec![pathfinder_search::SearchMatch {
file: "src/sandbox.rs".to_string(),
line: 2,
column: 4,
content: "pub fn check(&self) -> bool { true }".to_string(),
context_before: vec![],
context_after: vec![],
enclosing_semantic_path: None,
is_definition: None,
version_hash: "sha256:def".to_string(),
known: Some(false),
}],
total_matches: 1,
truncated: false,
files_searched: 1,
files_in_scope: 1,
binary_skipped: 0,
gitignored_skipped: 0,
other_skipped: 0,
}),
]);
let server = PathfinderServer::with_all_engines(
ws,
config,
sandbox,
scout,
surgeon,
Arc::new(pathfinder_lsp::NoOpLawyer),
);
let params = GetDefinitionParams {
semantic_path: "src/sandbox.rs::Sandbox.check".to_owned(),
};
let result = server.get_definition_impl(params).await;
let Ok(res) = result else {
panic!("expected Ok with grep_impl_method fallback, got Err");
};
let val = unpack_def(res);
assert!(val.degraded, "should be degraded");
assert_eq!(val.file, "src/sandbox.rs");
assert_eq!(val.line, 2);
assert_eq!(
val.degraded_reason,
Some(DegradedReason::GrepFallbackImplScoped),
"degraded_reason should be GrepFallbackImplScoped, got {:?}",
val.degraded_reason
);
assert_eq!(
val.resolution_strategy,
Some("grep_impl".to_owned()),
"resolution_strategy should be grep_impl"
);
}
}