use std::collections::HashMap;
use std::path::PathBuf;
use crate::base::FileId;
use crate::hir::{SymbolIndex, extract_symbols_unified};
use crate::syntax::SyntaxFile;
use super::{
CompletionItem, DocumentLink, FoldingRange, GotoResult, HoverResult, InlayHint,
ReferenceResult, SelectionRange, SemanticToken, SymbolInfo,
};
pub struct AnalysisHost {
files: HashMap<PathBuf, SyntaxFile>,
symbol_index: SymbolIndex,
file_id_map: HashMap<String, FileId>,
file_path_map: HashMap<FileId, String>,
index_dirty: bool,
}
impl Default for AnalysisHost {
fn default() -> Self {
Self::new()
}
}
impl AnalysisHost {
pub fn new() -> Self {
Self {
files: HashMap::new(),
symbol_index: SymbolIndex::new(),
file_id_map: HashMap::new(),
file_path_map: HashMap::new(),
index_dirty: false,
}
}
pub fn set_file_content(
&mut self,
path: &str,
content: &str,
) -> Vec<crate::parser::ParseError> {
use crate::syntax::parser::parse_with_result;
use std::path::Path;
let path_buf = PathBuf::from(path);
let result = parse_with_result(content, Path::new(path));
if let Some(syntax_file) = result.content {
self.files.insert(path_buf, syntax_file);
}
self.index_dirty = true;
result.errors
}
pub fn remove_file(&mut self, path: &str) {
let path_buf = PathBuf::from(path);
self.files.remove(&path_buf);
self.index_dirty = true;
}
pub fn remove_file_path(&mut self, path: &PathBuf) {
self.files.remove(path);
self.index_dirty = true;
}
pub fn has_file(&self, path: &str) -> bool {
let path_buf = PathBuf::from(path);
self.files.contains_key(&path_buf)
}
pub fn has_file_path(&self, path: &std::path::Path) -> bool {
self.files.contains_key(path)
}
pub fn set_file(&mut self, path: PathBuf, file: SyntaxFile) {
self.files.insert(path, file);
self.index_dirty = true;
}
pub fn files(&self) -> &HashMap<PathBuf, SyntaxFile> {
&self.files
}
pub fn file_count(&self) -> usize {
self.files.len()
}
pub fn mark_dirty(&mut self) {
self.index_dirty = true;
}
pub fn rebuild_index(&mut self) {
self.file_id_map.clear();
self.file_path_map.clear();
for (i, path) in self.files.keys().enumerate() {
let path_str = path.to_string_lossy().to_string();
let file_id = FileId::new(i as u32);
self.file_id_map.insert(path_str.clone(), file_id);
self.file_path_map.insert(file_id, path_str);
}
let mut new_index = SymbolIndex::new();
for (path, syntax_file) in &self.files {
let path_str = path.to_string_lossy().to_string();
if let Some(&file_id) = self.file_id_map.get(&path_str) {
let symbols = extract_symbols_unified(file_id, syntax_file);
new_index.add_file(file_id, symbols);
}
}
new_index.ensure_visibility_maps();
new_index.resolve_all_type_refs();
self.symbol_index = new_index;
self.index_dirty = false;
}
pub fn analysis(&mut self) -> Analysis<'_> {
if self.index_dirty {
self.rebuild_index();
}
Analysis {
symbol_index: &self.symbol_index,
file_id_map: &self.file_id_map,
file_path_map: &self.file_path_map,
}
}
pub fn get_file_id(&self, path: &str) -> Option<FileId> {
self.file_id_map.get(path).copied()
}
pub fn get_file_id_for_path(&self, path: &std::path::Path) -> Option<FileId> {
self.file_id_map
.get(&path.to_string_lossy().to_string())
.copied()
}
pub fn get_file_path(&self, file_id: FileId) -> Option<&str> {
self.file_path_map.get(&file_id).map(|s| s.as_str())
}
pub fn get_file_path_buf(&self, file_id: FileId) -> Option<PathBuf> {
self.file_path_map.get(&file_id).map(PathBuf::from)
}
pub fn file_id_map(&self) -> &HashMap<String, FileId> {
&self.file_id_map
}
pub fn symbol_index(&self) -> &SymbolIndex {
&self.symbol_index
}
}
pub struct Analysis<'a> {
symbol_index: &'a SymbolIndex,
file_id_map: &'a HashMap<String, FileId>,
file_path_map: &'a HashMap<FileId, String>,
}
impl<'a> Analysis<'a> {
pub fn hover(&self, file_id: FileId, line: u32, col: u32) -> Option<HoverResult> {
super::hover(self.symbol_index, file_id, line, col)
}
pub fn type_info_at(&self, file_id: FileId, line: u32, col: u32) -> Option<super::TypeInfo> {
super::type_info_at(self.symbol_index, file_id, line, col)
}
pub fn goto_definition(&self, file_id: FileId, line: u32, col: u32) -> GotoResult {
super::goto_definition(self.symbol_index, file_id, line, col)
}
pub fn goto_type_definition(&self, file_id: FileId, line: u32, col: u32) -> GotoResult {
super::goto_type_definition(self.symbol_index, file_id, line, col)
}
pub fn find_references(
&self,
file_id: FileId,
line: u32,
col: u32,
include_declaration: bool,
) -> ReferenceResult {
super::find_references(self.symbol_index, file_id, line, col, include_declaration)
}
pub fn completions(
&self,
file_id: FileId,
line: u32,
col: u32,
trigger: Option<char>,
) -> Vec<CompletionItem> {
super::completions(self.symbol_index, file_id, line, col, trigger)
}
pub fn document_symbols(&self, file_id: FileId) -> Vec<SymbolInfo> {
super::document_symbols(self.symbol_index, file_id)
}
pub fn workspace_symbols(&self, query: Option<&str>) -> Vec<SymbolInfo> {
super::workspace_symbols(self.symbol_index, query)
}
pub fn document_links(&self, file_id: FileId) -> Vec<DocumentLink> {
super::document_links(self.symbol_index, file_id)
}
pub fn folding_ranges(&self, file_id: FileId) -> Vec<FoldingRange> {
super::folding_ranges(self.symbol_index, file_id)
}
pub fn selection_ranges(&self, file_id: FileId, line: u32, col: u32) -> Vec<SelectionRange> {
super::selection_ranges(self.symbol_index, file_id, line, col)
}
pub fn inlay_hints(
&self,
file_id: FileId,
range: Option<(u32, u32, u32, u32)>,
) -> Vec<InlayHint> {
super::inlay_hints(self.symbol_index, file_id, range)
}
pub fn semantic_tokens(&self, file_id: FileId) -> Vec<SemanticToken> {
super::semantic_tokens(self.symbol_index, file_id)
}
pub fn symbol_index(&self) -> &SymbolIndex {
self.symbol_index
}
pub fn file_id_map(&self) -> &HashMap<String, FileId> {
self.file_id_map
}
pub fn get_file_path(&self, file_id: FileId) -> Option<&str> {
self.file_path_map.get(&file_id).map(|s| s.as_str())
}
pub fn get_file_id(&self, path: &str) -> Option<FileId> {
self.file_id_map.get(path).copied()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_analysis_host_basic() {
let mut host = AnalysisHost::new();
let errors = host.set_file_content("test.sysml", "package Test {}");
assert!(errors.is_empty());
let analysis = host.analysis();
assert!(analysis.get_file_id("test.sysml").is_some());
}
#[test]
fn test_file_removal() {
let mut host = AnalysisHost::new();
host.set_file_content("test.sysml", "package Test {}");
host.remove_file("test.sysml");
let analysis = host.analysis();
assert!(analysis.get_file_id("test.sysml").is_none());
}
}