use std::path::PathBuf;
use bock_air::{resolve_names_with_registry, ModuleRegistry, NameKind, SymbolTable};
use bock_errors::{FileId, Span};
use bock_lexer::Lexer;
use bock_parser::Parser;
use bock_source::SourceMap;
use crate::symbol_index::SymbolIndex;
pub struct DefinitionResult {
pub source_map: SourceMap,
pub file_id: FileId,
pub target: Span,
}
#[must_use]
pub fn find_definition(
path: PathBuf,
content: String,
line: u32,
character: u32,
) -> Option<DefinitionResult> {
let mut source_map = SourceMap::new();
let file_id = source_map.add_file(path, content);
let source_file = source_map.get_file(file_id);
let offset = position_to_offset(&source_file.content, line, character)?;
let mut lexer = Lexer::new(source_file);
let tokens = lexer.tokenize();
let mut parser = Parser::new(tokens, source_file);
let module = parser.parse_module();
let registry = ModuleRegistry::new();
let mut symbols = SymbolTable::new();
let _ = resolve_names_with_registry(&module, &mut symbols, ®istry);
let index = SymbolIndex::build(&module);
let target = index
.ident_use_at(offset)
.and_then(|(id, _)| symbols.resolutions.get(&id))
.filter(|r| r.kind != NameKind::Builtin)
.and_then(|r| index.def_spans.get(&r.def_id).copied())
.or_else(|| {
index
.type_segment_at(offset)
.and_then(|(name, _)| index.toplevel_by_name.get(name).map(|&(_, span)| span))
})?;
Some(DefinitionResult {
source_map,
file_id,
target,
})
}
#[must_use]
pub fn position_to_offset(content: &str, line: u32, character: u32) -> Option<usize> {
let mut line_start = 0usize;
if line > 0 {
let mut seen = 0u32;
let mut found = false;
for (i, ch) in content.char_indices() {
if ch == '\n' {
seen += 1;
if seen == line {
line_start = i + 1;
found = true;
break;
}
}
}
if !found {
return None;
}
}
let rest = &content[line_start..];
let mut byte_offset = 0usize;
for (counted, (i, ch)) in rest.char_indices().enumerate() {
if counted as u32 == character {
return Some(line_start + i);
}
if ch == '\n' {
return Some(line_start + i);
}
byte_offset = i + ch.len_utf8();
}
Some(line_start + byte_offset)
}
#[cfg(test)]
mod tests {
use super::*;
fn offset(content: &str, line: u32, ch: u32) -> usize {
position_to_offset(content, line, ch).expect("position valid")
}
#[test]
fn position_to_offset_start_of_file() {
assert_eq!(offset("hello\nworld", 0, 0), 0);
}
#[test]
fn position_to_offset_mid_first_line() {
assert_eq!(offset("hello\nworld", 0, 3), 3);
}
#[test]
fn position_to_offset_second_line() {
assert_eq!(offset("hello\nworld", 1, 2), 8); }
#[test]
fn position_to_offset_clamps_past_eol() {
assert_eq!(offset("ab\ncd", 0, 99), 2);
}
#[test]
fn position_to_offset_unknown_line_returns_none() {
assert!(position_to_offset("only one line", 5, 0).is_none());
}
#[test]
fn position_to_offset_multibyte() {
assert_eq!(offset("aéx", 0, 2), 3);
}
#[test]
fn definition_finds_fn_declaration_from_call_site() {
let src = "\
module m
public fn greet(name: String) -> String {
name
}
fn caller() -> String {
greet(\"world\")
}
";
let result = find_definition(PathBuf::from("test.bock"), src.to_string(), 7, 4)
.expect("definition found");
let source = result.source_map.get_file(result.file_id);
let (line, _) = source.line_col(result.target.start);
assert_eq!(line, 3, "target should point at the fn declaration line");
assert_eq!(source.slice(result.target), "greet");
}
#[test]
fn definition_finds_let_binding_from_use_site() {
let src = "\
module m
fn main() {
let answer = 42
answer
}
";
let result = find_definition(PathBuf::from("test.bock"), src.to_string(), 4, 4)
.expect("definition found");
let source = result.source_map.get_file(result.file_id);
assert_eq!(source.slice(result.target), "answer");
}
#[test]
fn definition_returns_none_for_unresolved_name() {
let src = "\
module m
fn main() {
undefined_name
}
";
assert!(find_definition(PathBuf::from("test.bock"), src.to_string(), 3, 4).is_none());
}
#[test]
fn definition_returns_none_for_builtin() {
let src = "\
module m
fn main() {
print(\"hi\")
}
";
assert!(find_definition(PathBuf::from("test.bock"), src.to_string(), 3, 4).is_none());
}
#[test]
fn definition_finds_type_declaration() {
let src = "\
module m
public record Point { x: Int, y: Int }
fn origin() -> Point {
Point { x: 0, y: 0 }
}
";
let result = find_definition(PathBuf::from("test.bock"), src.to_string(), 4, 17)
.expect("definition found");
let source = result.source_map.get_file(result.file_id);
assert_eq!(source.slice(result.target), "Point");
}
#[test]
fn definition_finds_enum_variant_constructor() {
let src = "\
module m
public enum Color { Red, Green, Blue }
fn favorite() -> Color {
Red
}
";
let result = find_definition(PathBuf::from("test.bock"), src.to_string(), 5, 4)
.expect("definition found");
let source = result.source_map.get_file(result.file_id);
assert_eq!(source.slice(result.target), "Red");
}
#[test]
fn definition_finds_param_type_from_annotation() {
let src = "\
module m
public record Point { x: Int, y: Int }
fn shift(p: Point) -> Int {
p.x
}
";
let result = find_definition(PathBuf::from("test.bock"), src.to_string(), 4, 12)
.expect("definition found");
let source = result.source_map.get_file(result.file_id);
assert_eq!(source.slice(result.target), "Point");
}
#[test]
fn definition_returns_none_when_cursor_is_off_any_identifier() {
let src = "\
module m
fn main() {
let x = 1
}
";
assert!(find_definition(PathBuf::from("test.bock"), src.to_string(), 3, 0).is_none());
}
}