Files
Kigi-CLI/crates/codegen/kigi-codebase-graph/src/navigation.rs
T
ZacharyZhang-NY d6c20fc13f M0: compilable skeleton — Kigi 0.1.0 fork surgery
Hard fork of xai-org/grok-build (Apache-2.0) re-targeted as Kigi, an
unofficial Kimi Code CLI community build.

Rename & identity
- 72 xai-*/xai-grok-* crates -> kigi-* (explicit: xai-grok-pager-bin ->
  kigi-bin [binary `kigi`], xai-grok-pager -> kigi-tui; rest mechanical);
  ptyctl, ptyctl-cli, third_party/ unchanged; proto package
  xai.grok.tools.v1 -> kigi.tools.v1
- Config home ~/.kigi (KIGI_SHARE_DIR override), env prefix GROK_* ->
  KIGI_*, `kigi --version` carries the unofficial-community-build notice
- clap identity, help text, startup banner, prompt templates rebranded
  (templates re-encrypted)

Deletions (PRD removal list #5/#6/#7/#9/#10)
- voice input (xai-grok-voice) and all TUI wiring
- telemetry: Mixpanel client, external OTel stream, Sentry, OTLP layers,
  trace/GCS/S3 upload queues (kigi-file-utils halved), workspace upload
  module & dc_log, heap-profile uploader, auth-diagnostics uploader,
  session-analytics halves of feedback; local zero-egress observability
  preserved in new kigi-log crate (unified log, --debug firehose,
  subsystem file logs, opt-in instrumentation)
- announcements (crate, remote-settings fields, TUI surfaces)
- plugin marketplace (crate, sources/browse/CTA/extensions-modal tab);
  direct plugin install/uninstall/update via kigi-agent git_install kept
- relay/gateway/assets endpoints and features (agent relay, headless
  relay transport, gateway bridge, LeaderEnvUrls); leader IPC socket now
  ~/.kigi/leader.sock + KIGI_LEADER_SOCKET, no ws-url derivation
- functional types rehomed instead of deleted: PermissionMode ->
  kigi-config-types, McpInitStrategy -> kigi-mcp, PrCreationSource ->
  session signals, TerminalDiagnostics -> kigi-pager-render, agent_id ->
  shell util

Endpoints
- kigi-env rewritten: single production KigiEndpoints {coding_api_base_url
  https://api.kimi.com/coding/v1 (KIGI_CODE_BASE_URL), oauth_host
  https://auth.kimi.com (KIGI_OAUTH_HOST), update_base_url (GitHub
  Releases API), upgrade_page_url}; GrokBuildEnvironment enum deleted

Toolchain & workspace hygiene
- Rust 1.97.0 pinned; edition 2024; full cargo update; git2 hoisted to
  workspace at 0.21 (Option->Result API migration), quick-xml 0.41
- Root Cargo.toml hand-maintained (PRD §8.1): version 0.1.0 inherited by
  all members, members sorted, unused deps pruned
- cargo-deny advisories gate (deny.toml with documented transitive
  exceptions); CI workflow (check/clippy/fmt/deny/test, macOS+Linux)
- cross-crate test seams re-gated behind `test-support` cargo feature;
  insta snapshot baselines renamed to the kigi_tui prefix
- clippy --workspace --all-targets: zero warnings; fmt clean

Fixes surfaced by the port
- updater probe/installer divergence (bin/kigi vs bin/grok symlink set)
- idle model-metadata refresh dead under KIGI_CODE_BASE_URL override
  (new is_effective_coding_endpoint_url, loopback+override aware)
- macOS symlinked-TMPDIR fixture canonicalization (foreign_sessions,
  fast-worktree); RSS measurement tests serialized via serial_test

Docs & legal (Apache §4)
- NOTICE added (upstream attribution + change statement); THIRD-PARTY
  notices sustained; kigi-tools ported-code notices extended; README,
  CONTRIBUTING, SECURITY, AGENTS.md rewritten

Out of scope for M0 (tracked): Kimi auth/inference (M1), search/fetch,
command parity, config import (M2), Computer Hub excision & final
brand-token sweep (M2), distribution & self-update rewrite (M3).
2026-07-17 05:31:01 -04:00

845 lines
27 KiB
Rust

//! Location-based navigation APIs for go-to-definition and go-to-references.
//!
//! This module provides APIs that take a file path and position (row, column)
//! and return definition or reference locations.
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::languages::LanguageRegistry;
use crate::scope_graph::ScopeGraphIndex;
/// Result of a navigation operation (go-to-definition or go-to-references).
#[derive(Debug, Clone)]
pub struct NavigationResult {
/// The symbol that was found at the query position.
pub symbol: String,
/// List of locations where the symbol is defined/referenced.
pub locations: Vec<Location>,
}
/// A location in a file.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Location {
/// Path to the file.
pub path: String,
/// 1-indexed line number.
pub line: usize,
/// Optional: the matched symbol name (useful for aliases).
pub symbol: Option<String>,
}
impl Location {
pub fn new(path: impl Into<String>, line: usize) -> Self {
Self {
path: path.into(),
line,
symbol: None,
}
}
pub fn with_symbol(path: impl Into<String>, line: usize, symbol: String) -> Self {
Self {
path: path.into(),
line,
symbol: Some(symbol),
}
}
/// Get the path as a Path reference.
pub fn as_path(&self) -> &Path {
Path::new(&self.path)
}
}
/// Error type for navigation operations.
#[derive(Debug)]
pub enum NavigationError {
/// File not found or could not be read.
FileNotFound(PathBuf),
/// Position is out of bounds for the file.
PositionOutOfBounds { row: usize, col: usize },
/// No symbol found at the given position.
NoSymbolAtPosition { row: usize, col: usize },
/// Language not supported for this file type.
UnsupportedLanguage(String),
/// Parse error.
ParseError(String),
/// IO error.
IoError(std::io::Error),
}
impl std::fmt::Display for NavigationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
NavigationError::FileNotFound(path) => {
write!(f, "File not found: {}", path.display())
}
NavigationError::PositionOutOfBounds { row, col } => {
write!(f, "Position out of bounds: {}:{}", row, col)
}
NavigationError::NoSymbolAtPosition { row, col } => {
write!(f, "No symbol found at position {}:{}", row, col)
}
NavigationError::UnsupportedLanguage(ext) => {
write!(f, "Unsupported language: {}", ext)
}
NavigationError::ParseError(msg) => write!(f, "Parse error: {}", msg),
NavigationError::IoError(e) => write!(f, "IO error: {}", e),
}
}
}
impl std::error::Error for NavigationError {}
impl From<std::io::Error> for NavigationError {
fn from(e: std::io::Error) -> Self {
NavigationError::IoError(e)
}
}
/// Navigator provides location-based code navigation.
///
/// It wraps a ScopeGraphIndex and provides methods to navigate code
/// based on file path and position (row, column).
pub struct Navigator {
index: Arc<ScopeGraphIndex>,
registry: LanguageRegistry,
}
impl Navigator {
/// Create a new Navigator backed by a shared index.
///
/// Accepts anything that converts into `Arc<ScopeGraphIndex>`, so both
/// owned and already-shared indexes work without extra wrapping:
///
/// ```rust,ignore
/// // From an owned index (e.g. IndexBuilder)
/// let navigator = Navigator::new(index);
///
/// // From a shared snapshot (zero-cost)
/// let snapshot = handle.get_snapshot()?;
/// let navigator = Navigator::new(snapshot);
/// ```
pub fn new(index: impl Into<Arc<ScopeGraphIndex>>) -> Self {
Self {
index: index.into(),
registry: LanguageRegistry::new(),
}
}
/// Get a reference to the underlying index.
pub fn index(&self) -> &ScopeGraphIndex {
&self.index
}
/// Get a mutable reference to the underlying index.
///
/// Uses copy-on-write: if other `Arc` clones of the index exist, the
/// index is cloned before returning the mutable reference.
pub fn index_mut(&mut self) -> &mut ScopeGraphIndex {
Arc::make_mut(&mut self.index)
}
/// Get the symbol at the given file path and position.
///
/// # Arguments
/// * `file_path` - Path to the file
/// * `row` - 1-indexed line number
/// * `col` - 1-indexed column number
///
/// # Returns
/// The symbol name at the given position.
pub fn get_symbol_at_position(
&self,
file_path: &Path,
row: usize,
col: usize,
) -> Result<String, NavigationError> {
// Validate position (1-indexed)
if row == 0 || col == 0 {
return Err(NavigationError::PositionOutOfBounds { row, col });
}
let content = std::fs::read(file_path)
.map_err(|_| NavigationError::FileNotFound(file_path.to_path_buf()))?;
// Get the language config
let lang_config = self.registry.for_file_path(file_path).ok_or_else(|| {
NavigationError::UnsupportedLanguage(
file_path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("unknown")
.to_string(),
)
})?;
// Parse the file
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&lang_config.language())
.map_err(|e| NavigationError::ParseError(format!("Failed to set language: {}", e)))?;
let tree = parser
.parse(&content, None)
.ok_or_else(|| NavigationError::ParseError("Failed to parse file".to_string()))?;
// Convert 1-indexed row/col to 0-indexed for tree-sitter
let point = tree_sitter::Point::new(row - 1, col - 1);
// Find the node at the position
let root = tree.root_node();
let node = find_smallest_named_node_at_point(root, point);
match node {
Some(n) => {
let text = std::str::from_utf8(&content[n.byte_range()])
.map_err(|_| NavigationError::ParseError("Invalid UTF-8".to_string()))?;
Ok(text.to_string())
}
None => Err(NavigationError::NoSymbolAtPosition { row, col }),
}
}
/// Go to definition for the symbol at the given position.
///
/// # Arguments
/// * `file_path` - Path to the file
/// * `row` - 1-indexed line number
/// * `col` - 1-indexed column number
///
/// # Returns
/// NavigationResult containing the symbol and its definition locations.
pub fn goto_definition(
&self,
file_path: &Path,
row: usize,
col: usize,
) -> Result<NavigationResult, NavigationError> {
let symbol = self.get_symbol_at_position(file_path, row, col)?;
// Look up definitions in the index
let defs =
self.index
.find_definitions_smart(&symbol, Some(file_path), Some(&self.registry));
let locations: Vec<Location> = defs
.into_iter()
.map(|(path, line)| Location::new(path, line))
.collect();
Ok(NavigationResult { symbol, locations })
}
/// Go to references for the symbol at the given position.
///
/// This first resolves the symbol to its definition, then finds all references.
///
/// # Arguments
/// * `file_path` - Path to the file
/// * `row` - 1-indexed line number
/// * `col` - 1-indexed column number
/// * `include_definition` - Whether to include the definition location in results
///
/// # Returns
/// NavigationResult containing the symbol and its reference locations.
pub fn goto_references(
&self,
file_path: &Path,
row: usize,
col: usize,
include_definition: bool,
) -> Result<NavigationResult, NavigationError> {
let symbol = self.get_symbol_at_position(file_path, row, col)?;
// Get references (includes alias resolution)
let refs = self
.index
.find_references_smart(&symbol, Some(file_path), Some(&self.registry));
let mut locations: Vec<Location> = refs
.into_iter()
.map(|(sym, path, line)| Location::with_symbol(path, line, sym))
.collect();
// Optionally include definition locations
if include_definition {
let defs =
self.index
.find_definitions_smart(&symbol, Some(file_path), Some(&self.registry));
for (path, line) in defs {
let loc = Location::new(path, line);
if !locations
.iter()
.any(|l| l.path == loc.path && l.line == loc.line)
{
locations.insert(0, loc);
}
}
}
Ok(NavigationResult { symbol, locations })
}
/// Go to definition by symbol name directly (without position lookup).
pub fn goto_definition_by_name(
&self,
symbol: &str,
context_file: Option<&Path>,
) -> NavigationResult {
let defs = self
.index
.find_definitions_smart(symbol, context_file, Some(&self.registry));
let locations: Vec<Location> = defs
.into_iter()
.map(|(path, line)| Location::new(path, line))
.collect();
NavigationResult {
symbol: symbol.to_string(),
locations,
}
}
/// Go to references by symbol name directly (without position lookup).
pub fn goto_references_by_name(
&self,
symbol: &str,
context_file: Option<&Path>,
include_definition: bool,
) -> NavigationResult {
let refs = self
.index
.find_references_smart(symbol, context_file, Some(&self.registry));
let mut locations: Vec<Location> = refs
.into_iter()
.map(|(sym, path, line)| Location::with_symbol(path, line, sym))
.collect();
if include_definition {
let defs =
self.index
.find_definitions_smart(symbol, context_file, Some(&self.registry));
for (path, line) in defs {
let loc = Location::new(path, line);
if !locations
.iter()
.any(|l| l.path == loc.path && l.line == loc.line)
{
locations.insert(0, loc);
}
}
}
NavigationResult {
symbol: symbol.to_string(),
locations,
}
}
}
/// Find the smallest named node that contains the given point.
fn find_smallest_named_node_at_point(
node: tree_sitter::Node<'_>,
point: tree_sitter::Point,
) -> Option<tree_sitter::Node<'_>> {
// Check if point is within this node
if point < node.start_position() || point > node.end_position() {
return None;
}
// Try to find a smaller child node that contains the point
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if let Some(found) = find_smallest_named_node_at_point(child, point) {
// Prefer named nodes that look like identifiers
if is_identifier_like(&found) {
return Some(found);
}
// Keep searching for a better match
if is_identifier_like(&child) {
return Some(child);
}
return Some(found);
}
}
// No smaller child contains the point, return this node if it's identifier-like
if is_identifier_like(&node) {
Some(node)
} else {
None
}
}
/// Check if a node looks like an identifier.
fn is_identifier_like(node: &tree_sitter::Node<'_>) -> bool {
let kind = node.kind();
matches!(
kind,
"identifier"
| "type_identifier"
| "property_identifier"
| "field_identifier"
| "shorthand_property_identifier"
| "shorthand_property_identifier_pattern"
| "attribute" // Python
| "package_identifier" // Go
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::IndexBuilder;
use std::fs::File;
use std::io::Write;
use tempfile::tempdir;
#[test]
fn test_get_symbol_at_position() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.rs");
let mut file = File::create(&file_path).unwrap();
writeln!(file, "fn hello_world() {{").unwrap();
writeln!(file, " println!(\"Hello\");").unwrap();
writeln!(file, "}}").unwrap();
let index = IndexBuilder::new().build(dir.path()).unwrap();
let navigator = Navigator::new(index);
// Get symbol at "hello_world" (row 1, col 4)
let symbol = navigator.get_symbol_at_position(&file_path, 1, 4).unwrap();
assert_eq!(symbol, "hello_world");
}
#[test]
fn test_typescript_for_of_array_destructuring() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.ts");
let mut file = File::create(&file_path).unwrap();
writeln!(
file,
r#"const myMap = new Map<string, {{ sessionId: string }}>();
function test(sessionId: string) {{
for (const [toolCallId, request] of myMap) {{
if (request.sessionId !== sessionId) continue;
console.log(toolCallId);
}}
}}"#
)
.unwrap();
drop(file);
let index = IndexBuilder::new().build(dir.path()).unwrap();
let navigator = Navigator::new(index);
// Test that 'request' is found as a definition at line 4
let def_result = navigator.goto_definition_by_name("request", Some(&file_path));
assert!(
!def_result.locations.is_empty(),
"request should be found as a definition"
);
assert_eq!(
def_result.locations[0].line, 4,
"request should be defined on line 4"
);
// Test that 'toolCallId' is also found as a definition at line 4
let def_result2 = navigator.goto_definition_by_name("toolCallId", Some(&file_path));
assert!(
!def_result2.locations.is_empty(),
"toolCallId should be found as a definition"
);
assert_eq!(
def_result2.locations[0].line, 4,
"toolCallId should be defined on line 4"
);
// Test that references to 'request' are found
let ref_result = navigator.goto_references_by_name("request", Some(&file_path), false);
assert!(
!ref_result.locations.is_empty(),
"request should have references"
);
// Check that line 5 reference is found (request.sessionId)
let line5_ref = ref_result.locations.iter().any(|loc| loc.line == 5);
assert!(line5_ref, "request should be referenced on line 5");
}
#[test]
fn test_typescript_for_of_object_destructuring() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.ts");
let mut file = File::create(&file_path).unwrap();
writeln!(
file,
r#"const items = [{{ name: "a", value: 1 }}];
for (const {{ name, value }} of items) {{
console.log(name, value);
}}"#
)
.unwrap();
drop(file);
let index = IndexBuilder::new().build(dir.path()).unwrap();
let navigator = Navigator::new(index);
// Test that 'name' is found as a definition at line 3
let def_result = navigator.goto_definition_by_name("name", Some(&file_path));
assert!(
!def_result.locations.is_empty(),
"name should be found as a definition"
);
assert_eq!(
def_result.locations[0].line, 3,
"name should be defined on line 3"
);
// Test that 'value' is found as a definition at line 3
let def_result2 = navigator.goto_definition_by_name("value", Some(&file_path));
assert!(
!def_result2.locations.is_empty(),
"value should be found as a definition"
);
assert_eq!(
def_result2.locations[0].line, 3,
"value should be defined on line 3"
);
}
#[test]
fn test_typescript_regular_array_destructuring() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.ts");
let mut file = File::create(&file_path).unwrap();
writeln!(
file,
r#"const arr = [1, 2, 3];
const [first, second, third] = arr;
console.log(first, second);"#
)
.unwrap();
drop(file);
let index = IndexBuilder::new().build(dir.path()).unwrap();
let navigator = Navigator::new(index);
// Test that 'first' is found as a definition at line 2
let def_result = navigator.goto_definition_by_name("first", Some(&file_path));
assert!(
!def_result.locations.is_empty(),
"first should be found as a definition"
);
assert_eq!(
def_result.locations[0].line, 2,
"first should be defined on line 2"
);
// Test that 'second' is found as a definition at line 2
let def_result2 = navigator.goto_definition_by_name("second", Some(&file_path));
assert!(
!def_result2.locations.is_empty(),
"second should be found as a definition"
);
assert_eq!(
def_result2.locations[0].line, 2,
"second should be defined on line 2"
);
}
#[test]
fn test_typescript_regular_object_destructuring() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.ts");
let mut file = File::create(&file_path).unwrap();
writeln!(
file,
r#"const obj = {{ foo: 1, bar: 2 }};
const {{ foo, bar }} = obj;
console.log(foo, bar);"#
)
.unwrap();
drop(file);
let index = IndexBuilder::new().build(dir.path()).unwrap();
let navigator = Navigator::new(index);
// Test that 'foo' is found as a definition at line 2
let def_result = navigator.goto_definition_by_name("foo", Some(&file_path));
assert!(
!def_result.locations.is_empty(),
"foo should be found as a definition"
);
assert_eq!(
def_result.locations[0].line, 2,
"foo should be defined on line 2"
);
// Test that 'bar' is found as a definition at line 2
let def_result2 = navigator.goto_definition_by_name("bar", Some(&file_path));
assert!(
!def_result2.locations.is_empty(),
"bar should be found as a definition"
);
assert_eq!(
def_result2.locations[0].line, 2,
"bar should be defined on line 2"
);
}
#[test]
fn test_typescript_member_expression_reference() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.ts");
let mut file = File::create(&file_path).unwrap();
writeln!(
file,
r#"const myObject = {{ value: 42 }};
const result = myObject.value;"#
)
.unwrap();
drop(file);
let index = IndexBuilder::new().build(dir.path()).unwrap();
let navigator = Navigator::new(index);
// Test that 'myObject' is referenced on line 2 (myObject.value)
let ref_result = navigator.goto_references_by_name("myObject", Some(&file_path), false);
assert!(
!ref_result.locations.is_empty(),
"myObject should have references"
);
let line2_ref = ref_result.locations.iter().any(|loc| loc.line == 2);
assert!(line2_ref, "myObject should be referenced on line 2");
}
#[test]
fn test_typescript_function_parameter_destructuring() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.ts");
let mut file = File::create(&file_path).unwrap();
writeln!(
file,
r#"function process([first, second]: number[], {{ name }}: {{ name: string }}) {{
console.log(first, second, name);
}}"#
)
.unwrap();
drop(file);
let index = IndexBuilder::new().build(dir.path()).unwrap();
let navigator = Navigator::new(index);
// Test that 'first' is found as a definition at line 1
let def_result = navigator.goto_definition_by_name("first", Some(&file_path));
assert!(
!def_result.locations.is_empty(),
"first should be found as a definition"
);
assert_eq!(
def_result.locations[0].line, 1,
"first should be defined on line 1"
);
// Test that 'name' is found as a definition at line 1
let def_result2 = navigator.goto_definition_by_name("name", Some(&file_path));
assert!(
!def_result2.locations.is_empty(),
"name should be found as a definition"
);
assert_eq!(
def_result2.locations[0].line, 1,
"name should be defined on line 1"
);
}
#[test]
fn test_typescript_simple_function_parameters() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.ts");
let mut file = File::create(&file_path).unwrap();
writeln!(
file,
r#"function greet(name: string, age: number) {{
console.log(name, age);
}}"#
)
.unwrap();
drop(file);
let index = IndexBuilder::new().build(dir.path()).unwrap();
let navigator = Navigator::new(index);
// Test that 'name' is found as a definition at line 1
let def_result = navigator.goto_definition_by_name("name", Some(&file_path));
assert!(
!def_result.locations.is_empty(),
"name should be found as a definition"
);
assert_eq!(
def_result.locations[0].line, 1,
"name should be defined on line 1"
);
// Test that 'age' is found as a definition at line 1
let def_result2 = navigator.goto_definition_by_name("age", Some(&file_path));
assert!(
!def_result2.locations.is_empty(),
"age should be found as a definition"
);
assert_eq!(
def_result2.locations[0].line, 1,
"age should be defined on line 1"
);
}
#[test]
fn test_typescript_react_dependency_array_references() {
// Test that identifiers in React hook dependency arrays are captured as references
// Regression test for dependency-array reference capture.
let dir = tempdir().unwrap();
let file_path = dir.path().join("component.tsx");
let mut file = File::create(&file_path).unwrap();
writeln!(
file,
r#"import {{ useCallback, useEffect }} from 'react';
function FileTreeTab({{ basePath, onFileSelect }}) {{
const fileTree = useFileTree();
const loadDirectory = useCallback(
(path: string) => {{
return fileTree.listFiles(path);
}},
[fileTree],
);
const handleOpenPath = useCallback(
(path: string) => {{
loadDirectory(path);
}},
[loadDirectory, fileTree],
);
const handleFileSelect = useCallback(
(file) => {{
fileTree.setSelectedPath(file.absPath);
onFileSelect(file.absPath);
}},
[fileTree, onFileSelect],
);
useEffect(() => {{
loadDirectory(basePath);
}}, [basePath, loadDirectory]);
return null;
}}"#
)
.unwrap();
drop(file);
let index = IndexBuilder::new().build(dir.path()).unwrap();
let navigator = Navigator::new(index);
// Test that 'fileTree' is found in dependency arrays
let ref_result = navigator.goto_references_by_name("fileTree", Some(&file_path), false);
assert!(
!ref_result.locations.is_empty(),
"fileTree should have references"
);
// Check references in dependency arrays:
// Line 10: [fileTree]
// Line 17: [loadDirectory, fileTree]
// Line 25: [fileTree, onFileSelect]
let dep_array_lines: Vec<usize> = ref_result
.locations
.iter()
.filter(|loc| loc.line == 10 || loc.line == 17 || loc.line == 25)
.map(|loc| loc.line)
.collect();
assert!(
dep_array_lines.contains(&10),
"fileTree should be referenced on line 10 (first dependency array)"
);
assert!(
dep_array_lines.contains(&17),
"fileTree should be referenced on line 17 (second dependency array)"
);
assert!(
dep_array_lines.contains(&25),
"fileTree should be referenced on line 25 (third dependency array)"
);
// Test that 'loadDirectory' is found in dependency arrays
let ref_result2 =
navigator.goto_references_by_name("loadDirectory", Some(&file_path), false);
assert!(
!ref_result2.locations.is_empty(),
"loadDirectory should have references"
);
// Line 17: [loadDirectory, fileTree]
// Line 30: [basePath, loadDirectory]
let load_dir_refs: Vec<usize> = ref_result2
.locations
.iter()
.filter(|loc| loc.line == 17 || loc.line == 30)
.map(|loc| loc.line)
.collect();
assert!(
load_dir_refs.contains(&17),
"loadDirectory should be referenced on line 17"
);
assert!(
load_dir_refs.contains(&30),
"loadDirectory should be referenced on line 30"
);
// Test that 'onFileSelect' is found in dependency array
let ref_result3 =
navigator.goto_references_by_name("onFileSelect", Some(&file_path), false);
assert!(
!ref_result3.locations.is_empty(),
"onFileSelect should have references"
);
let on_file_select_line25 = ref_result3.locations.iter().any(|loc| loc.line == 25);
assert!(
on_file_select_line25,
"onFileSelect should be referenced on line 25 (dependency array)"
);
// Test that 'basePath' is found in dependency array
let ref_result4 = navigator.goto_references_by_name("basePath", Some(&file_path), false);
assert!(
!ref_result4.locations.is_empty(),
"basePath should have references"
);
let base_path_line30 = ref_result4.locations.iter().any(|loc| loc.line == 30);
assert!(
base_path_line30,
"basePath should be referenced on line 30 (useEffect dependency array)"
);
}
}