use crate::error::SourceLocation;
use std::collections::{BTreeMap, HashSet};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReferenceKind {
Definition,
Usage,
TypeReference,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Reference {
pub kind: ReferenceKind,
pub location: SourceLocation,
pub start_offset: u32,
pub end_offset: u32,
pub end_column: usize,
}
impl Reference {
pub fn new(
kind: ReferenceKind,
location: SourceLocation,
start_offset: u32,
end_offset: u32,
) -> Self {
let width = (end_offset - start_offset) as usize;
Self {
kind,
end_column: location.column + width,
location,
start_offset,
end_offset,
}
}
pub fn end_column(&self) -> usize {
self.end_column
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SymbolKey {
pub scope_id: usize,
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct PositionedReference {
file: String,
line: usize,
start_column: usize,
end_column: usize,
key: SymbolKey,
}
#[derive(Debug, Clone, Default)]
pub struct QueryIndex {
by_symbol: BTreeMap<SymbolKey, Vec<Reference>>,
recorded_ranges: HashSet<(String, u32, u32)>,
by_position: Vec<PositionedReference>,
}
impl QueryIndex {
pub fn new() -> Self {
Self::default()
}
pub fn record(&mut self, scope_id: usize, name: &str, reference: Reference) {
self.recorded_ranges.insert((
reference.location.file.clone(),
reference.start_offset,
reference.end_offset,
));
let key = SymbolKey {
scope_id,
name: name.to_lowercase(),
};
self.by_symbol
.entry(key.clone())
.or_default()
.push(reference.clone());
self.by_position.push(PositionedReference {
file: reference.location.file,
line: reference.location.line,
start_column: reference.location.column,
end_column: reference.end_column,
key,
});
}
pub fn is_recorded(&self, file: &str, start: u32, end: u32) -> bool {
self.recorded_ranges
.contains(&(file.to_string(), start, end))
}
pub fn finalize(&mut self) {
self.by_position.sort_by(|a, b| {
a.file
.cmp(&b.file)
.then(a.line.cmp(&b.line))
.then(a.start_column.cmp(&b.start_column))
});
}
pub fn references_for(&self, scope_id: usize, name: &str) -> Option<&Vec<Reference>> {
let key = SymbolKey {
scope_id,
name: name.to_lowercase(),
};
self.by_symbol.get(&key)
}
pub fn symbol_at(&self, file: &str, line: usize, column: usize) -> Option<&SymbolKey> {
let start = self.by_position.partition_point(|p| {
p.file.as_str() < file || (p.file.as_str() == file && p.line < line)
});
for positioned in &self.by_position[start..] {
if positioned.file != file || positioned.line != line {
break;
}
if column >= positioned.start_column && column < positioned.end_column {
return Some(&positioned.key);
}
}
None
}
pub fn references_at(&self, file: &str, line: usize, column: usize) -> Option<&Vec<Reference>> {
let key = self.symbol_at(file, line, column)?;
self.by_symbol.get(key)
}
pub fn definition_at(&self, file: &str, line: usize, column: usize) -> Option<&Reference> {
self.references_at(file, line, column)
.and_then(|references| {
references
.iter()
.find(|r| r.kind == ReferenceKind::Definition)
})
}
pub fn iter(&self) -> impl Iterator<Item = (&SymbolKey, &Vec<Reference>)> {
self.by_symbol.iter()
}
pub fn len(&self) -> usize {
self.by_position.len()
}
pub fn is_empty(&self) -> bool {
self.by_position.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn location(file: &str, line: usize, column: usize) -> SourceLocation {
SourceLocation {
file: file.to_string(),
line,
column,
}
}
fn record(
index: &mut QueryIndex,
scope: usize,
name: &str,
kind: ReferenceKind,
line: usize,
col: usize,
) {
let reference = Reference::new(kind, location("M.bas", line, col), 10, 14);
index.record(scope, name, reference);
}
#[test]
fn references_for_is_case_insensitive() {
let mut index = QueryIndex::new();
record(&mut index, 1, "Counter", ReferenceKind::Definition, 1, 1);
record(&mut index, 1, "Counter", ReferenceKind::Usage, 5, 3);
assert_eq!(index.references_for(1, "counter").unwrap().len(), 2);
assert_eq!(index.references_for(1, "COUNTER").unwrap().len(), 2);
assert!(index.references_for(2, "Counter").is_none());
}
#[test]
fn symbol_at_matches_identifier_span() {
let mut index = QueryIndex::new();
record(&mut index, 1, "Foo", ReferenceKind::Definition, 3, 7);
index.finalize();
let key = index.symbol_at("M.bas", 3, 7).expect("start column");
assert_eq!(key.name, "foo");
let key = index.symbol_at("M.bas", 3, 10).expect("end column");
assert_eq!(key.name, "foo");
assert!(index.symbol_at("M.bas", 3, 6).is_none());
assert!(index.symbol_at("M.bas", 3, 11).is_none());
assert!(index.symbol_at("M.bas", 4, 7).is_none());
assert!(index.symbol_at("other.bas", 3, 7).is_none());
}
#[test]
fn references_at_and_definition_at() {
let mut index = QueryIndex::new();
record(&mut index, 1, "Helper", ReferenceKind::Definition, 2, 5);
record(&mut index, 1, "Helper", ReferenceKind::Usage, 9, 2);
index.finalize();
let refs = index.references_at("M.bas", 9, 2).unwrap();
assert_eq!(refs.len(), 2);
assert_eq!(refs[1].kind, ReferenceKind::Usage);
let def = index.definition_at("M.bas", 9, 2).unwrap();
assert_eq!(def.kind, ReferenceKind::Definition);
assert_eq!(def.location.line, 2);
}
#[test]
fn is_recorded_tracks_recorded_ranges() {
let mut index = QueryIndex::new();
record(&mut index, 1, "Foo", ReferenceKind::Definition, 1, 1);
record(&mut index, 1, "Foo", ReferenceKind::Usage, 5, 3);
assert!(index.is_recorded("M.bas", 10, 14));
assert!(!index.is_recorded("M.bas", 10, 15));
assert!(!index.is_recorded("N.bas", 10, 14));
}
}