use tower_lsp::lsp_types::{Location, Position, Url};
use crate::analysis::SymbolTable;
pub fn get_references(
symbol_table: &SymbolTable,
source: &str,
position: Position,
uri: &Url,
include_declaration: bool,
) -> Vec<Location> {
let identifier = match find_identifier_at_position(source, position) {
Some(id) => id,
None => return Vec::new(),
};
let scope_id = symbol_table.scope_at_position(position);
let symbol_id = match symbol_table.lookup(&identifier, scope_id) {
Some(id) => id,
None => return Vec::new(),
};
let symbol = match symbol_table.get_symbol(symbol_id) {
Some(s) => s,
None => return Vec::new(),
};
let mut locations = Vec::new();
if include_declaration {
locations.push(Location {
uri: uri.clone(),
range: symbol.name_range,
});
}
for range in &symbol.references {
locations.push(Location {
uri: uri.clone(),
range: *range,
});
}
locations
}
fn find_identifier_at_position(source: &str, position: Position) -> Option<String> {
let lines: Vec<&str> = source.lines().collect();
let line_idx = position.line as usize;
if line_idx >= lines.len() {
return None;
}
let line = lines[line_idx];
let col = position.character as usize;
if col > line.len() {
return None;
}
let chars: Vec<char> = line.chars().collect();
let mut start = col;
while start > 0 && is_identifier_char(chars.get(start - 1).copied()) {
start -= 1;
}
let mut end = col;
while end < chars.len() && is_identifier_char(chars.get(end).copied()) {
end += 1;
}
if start == end {
return None;
}
let identifier: String = chars[start..end].iter().collect();
if is_valid_identifier(&identifier) {
Some(identifier)
} else {
None
}
}
fn is_identifier_char(c: Option<char>) -> bool {
match c {
Some(c) => c.is_alphanumeric() || c == '_' || c == '$',
None => false,
}
}
fn is_valid_identifier(s: &str) -> bool {
if s.is_empty() {
return false;
}
let mut chars = s.chars();
let first = chars.next().unwrap();
if !first.is_alphabetic() && first != '_' && first != '$' {
return false;
}
chars.all(|c| c.is_alphanumeric() || c == '_' || c == '$')
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analysis::{SymbolFlags, SymbolTable};
use tower_lsp::lsp_types::Range;
fn create_test_uri() -> Url {
Url::parse("file:///test/test.ts").unwrap()
}
#[test]
fn test_is_identifier_char() {
assert!(is_identifier_char(Some('a')));
assert!(is_identifier_char(Some('Z')));
assert!(is_identifier_char(Some('0')));
assert!(is_identifier_char(Some('_')));
assert!(is_identifier_char(Some('$')));
assert!(!is_identifier_char(Some(' ')));
assert!(!is_identifier_char(None));
}
#[test]
fn test_is_valid_identifier() {
assert!(is_valid_identifier("x"));
assert!(is_valid_identifier("myVar"));
assert!(is_valid_identifier("_private"));
assert!(is_valid_identifier("$jquery"));
assert!(!is_valid_identifier(""));
assert!(!is_valid_identifier("123abc"));
}
#[test]
fn test_get_references_with_declaration() {
let mut table = SymbolTable::new();
let uri = create_test_uri();
let source = "const x = 1;";
let decl_range = Range {
start: Position::new(0, 6),
end: Position::new(0, 7),
};
let id = table.create_symbol(
"x".to_string(),
SymbolFlags::VARIABLE | SymbolFlags::CONST,
decl_range,
decl_range,
0,
);
let ref_range = Range {
start: Position::new(1, 0),
end: Position::new(1, 1),
};
table.add_reference(id, ref_range);
let refs = get_references(&table, source, Position::new(0, 6), &uri, true);
assert_eq!(refs.len(), 2);
}
#[test]
fn test_get_references_without_declaration() {
let mut table = SymbolTable::new();
let uri = create_test_uri();
let source = "const x = 1;";
let decl_range = Range {
start: Position::new(0, 6),
end: Position::new(0, 7),
};
let id = table.create_symbol(
"x".to_string(),
SymbolFlags::VARIABLE,
decl_range,
decl_range,
0,
);
let ref_range = Range {
start: Position::new(1, 0),
end: Position::new(1, 1),
};
table.add_reference(id, ref_range);
let refs = get_references(&table, source, Position::new(0, 6), &uri, false);
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].range, ref_range);
}
#[test]
fn test_get_references_unknown_symbol() {
let table = SymbolTable::new();
let uri = create_test_uri();
let source = "const x = unknownVar;";
let refs = get_references(&table, source, Position::new(0, 15), &uri, true);
assert!(refs.is_empty());
}
#[test]
fn test_get_references_no_identifier() {
let table = SymbolTable::new();
let uri = create_test_uri();
let source = "const x = 42;";
let refs = get_references(&table, source, Position::new(0, 8), &uri, true);
let _ = refs;
}
#[test]
fn test_get_references_multiple_references() {
let mut table = SymbolTable::new();
let uri = create_test_uri();
let source = "const x = 1;\nconsole.log(x);\nreturn x;";
let decl_range = Range {
start: Position::new(0, 6),
end: Position::new(0, 7),
};
let id = table.create_symbol(
"x".to_string(),
SymbolFlags::VARIABLE,
decl_range,
decl_range,
0,
);
table.add_reference(
id,
Range {
start: Position::new(1, 12),
end: Position::new(1, 13),
},
);
table.add_reference(
id,
Range {
start: Position::new(2, 7),
end: Position::new(2, 8),
},
);
let refs = get_references(&table, source, Position::new(0, 6), &uri, true);
assert_eq!(refs.len(), 3);
}
#[test]
fn test_find_identifier_at_position_simple() {
let source = "const myVar = 42;";
let result = find_identifier_at_position(source, Position::new(0, 8));
assert_eq!(result, Some("myVar".to_string()));
}
#[test]
fn test_find_identifier_at_position_out_of_bounds() {
let source = "const x = 42;";
let result = find_identifier_at_position(source, Position::new(10, 0));
assert!(result.is_none());
}
#[test]
fn test_references_all_have_same_uri() {
let mut table = SymbolTable::new();
let uri = create_test_uri();
let source = "const x = 1;";
let range = Range {
start: Position::new(0, 6),
end: Position::new(0, 7),
};
let id = table.create_symbol("x".to_string(), SymbolFlags::VARIABLE, range, range, 0);
table.add_reference(id, range);
let refs = get_references(&table, source, Position::new(0, 6), &uri, true);
for r in refs {
assert_eq!(r.uri, uri);
}
}
}