use crate::{
HoverInfo, Position, Range, Reference, SemanticJudge, SignatureHelp, TypeInfo,
};
use ra_ap_base_db::{
CrateGraph, CrateOrigin, Env, SourceDatabase, SourceDatabaseFileInputExt, SourceRoot,
SourceRootDatabase,
};
use ra_ap_ide::{
Analysis, AnalysisHost, Edition, FilePosition, FileRange, HoverConfig, HoverDocFormat,
};
use ra_ap_ide_db::base_db::salsa::Durability;
use ra_ap_syntax::TextSize;
use ra_ap_vfs::{file_set::FileSet, FileId, VfsPath};
use std::cell::RefCell;
use std::collections::HashMap;
use triomphe::Arc;
use super::rust::RustJudge;
pub struct SemanticRustJudge {
base: RustJudge,
host: RefCell<AnalysisHost>,
file_ids: RefCell<HashMap<String, FileId>>,
next_file_id: RefCell<u32>,
has_workspace: bool,
}
impl SemanticRustJudge {
pub fn new() -> Self {
let current_dir = std::env::current_dir().ok();
if let Some(dir) = current_dir {
if let Ok(judge) = Self::with_workspace(&dir) {
return judge;
}
}
Self {
base: RustJudge::new(),
host: RefCell::new(AnalysisHost::default()),
file_ids: RefCell::new(HashMap::new()),
next_file_id: RefCell::new(0),
has_workspace: false,
}
}
pub fn with_workspace(project_path: &std::path::Path) -> Result<Self, String> {
use ra_ap_load_cargo::{
load_workspace_at, LoadCargoConfig, ProcMacroServerChoice,
};
use ra_ap_project_model::CargoConfig;
let cargo_config = CargoConfig::default();
let load_config = LoadCargoConfig {
load_out_dirs_from_check: true,
with_proc_macro_server: ProcMacroServerChoice::Sysroot,
prefill_caches: false,
};
match load_workspace_at(project_path, &cargo_config, &load_config, &|_| {}) {
Ok((db, _vfs, _proc_macro)) => {
let host = AnalysisHost::with_database(db);
Ok(Self {
base: RustJudge::new(),
host: RefCell::new(host),
file_ids: RefCell::new(HashMap::new()),
next_file_id: RefCell::new(0),
has_workspace: true,
})
}
Err(e) => {
eprintln!("Warning: Failed to load Cargo workspace: {:?}", e);
Ok(Self::new())
}
}
}
fn update_file(&self, file_path: &str, source: &str) -> Result<FileId, String> {
let mut file_ids = self.file_ids.borrow_mut();
let mut host = self.host.borrow_mut();
let file_id = if let Some(&existing_id) = file_ids.get(file_path) {
existing_id
} else {
let mut next_id = self.next_file_id.borrow_mut();
let file_id = FileId::from_raw(*next_id);
*next_id += 1;
file_ids.insert(file_path.to_string(), file_id);
if !self.has_workspace {
let db = host.raw_database_mut();
let vfs_path = VfsPath::new_virtual_path(file_path.to_string());
let source_root_id = ra_ap_base_db::SourceRootId(0);
let mut file_set = FileSet::default();
for (path, &id) in file_ids.iter() {
file_set.insert(id, VfsPath::new_virtual_path(path.clone()));
}
let source_root = SourceRoot::new_local(file_set);
db.set_source_root_with_durability(
source_root_id,
Arc::new(source_root),
Durability::HIGH,
);
db.set_file_source_root_with_durability(
file_id,
source_root_id,
Durability::HIGH,
);
}
file_id
};
let db = host.raw_database_mut();
db.set_file_text_with_durability(file_id, source, Durability::HIGH);
Ok(file_id)
}
fn create_analysis(&self, source: &str) -> Result<(Analysis, FileId), String> {
let file_id = self.update_file("/main.rs", source)?;
if !self.has_workspace {
let file_ids = self.file_ids.borrow();
if file_ids.len() == 1 {
let mut host = self.host.borrow_mut();
let db = host.raw_database_mut();
let mut crate_graph = CrateGraph::default();
crate_graph.add_crate_root(
file_id,
Edition::Edition2021,
None,
None,
Arc::new(ra_ap_hir::CfgOptions::default()),
Some(Arc::new(ra_ap_hir::CfgOptions::default())),
Env::default(),
false,
CrateOrigin::Local {
repo: None,
name: None,
},
);
db.set_crate_graph_with_durability(
Arc::new(crate_graph),
Durability::HIGH,
);
}
}
let host = self.host.borrow();
Ok((host.analysis(), file_id))
}
fn position_to_offset(&self, source: &str, position: Position) -> Option<TextSize> {
let mut current_line = 0;
let mut current_col = 0;
for (idx, ch) in source.chars().enumerate() {
if current_line == position.line && current_col == position.column {
return Some(TextSize::from(idx as u32));
}
if ch == '\n' {
current_line += 1;
current_col = 0;
} else {
current_col += 1;
}
}
None
}
fn text_size_to_position(&self, source: &str, offset: TextSize) -> Position {
let offset_usize = usize::from(offset);
let mut line = 0;
let mut column = 0;
for (idx, ch) in source.chars().enumerate() {
if idx >= offset_usize {
break;
}
if ch == '\n' {
line += 1;
column = 0;
} else {
column += 1;
}
}
Position { line, column }
}
}
impl Default for SemanticRustJudge {
fn default() -> Self {
Self::new()
}
}
impl crate::Judge for SemanticRustJudge {
fn diagnostics(&self, source: &str) -> Vec<crate::Diagnostic> {
self.base.diagnostics(source)
}
fn symbols(&self, source: &str) -> Result<Vec<crate::Symbol>, String> {
self.base.symbols(source)
}
fn symbol_at_position(
&self,
source: &str,
position: Position,
) -> Result<Option<crate::Symbol>, String> {
self.base.symbol_at_position(source, position)
}
fn completions(
&self,
source: &str,
position: Position,
) -> Result<Vec<crate::CompletionItem>, String> {
let prefix = RustJudge::extract_word_prefix(source, position);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let (analysis, file_id) = self.create_analysis(source)?;
let offset = self
.position_to_offset(source, position)
.ok_or_else(|| "Invalid position".to_string())?;
let file_position = FilePosition { file_id, offset };
let config = ra_ap_ide::CompletionConfig {
enable_postfix_completions: true,
enable_imports_on_the_fly: false,
enable_self_on_the_fly: true,
enable_private_editable: false,
add_semicolon_to_unit: false,
enable_term_search: false,
full_function_signatures: false,
callable: None,
snippet_cap: None,
prefer_no_std: false,
prefer_prelude: true,
prefer_absolute: false,
limit: None,
term_search_fuel: 400,
insert_use: ra_ap_ide_db::imports::insert_use::InsertUseConfig {
granularity:
ra_ap_ide_db::imports::insert_use::ImportGranularity::Crate,
enforce_granularity: true,
prefix_kind: ra_ap_ide_db::imports::insert_use::PrefixKind::Plain,
group: true,
skip_glob_imports: true,
},
snippets: Vec::new(),
};
let completions = analysis
.completions(&config, file_position, None)
.map_err(|e| format!("Completion analysis cancelled: {:?}", e))?;
match completions {
Some(completions) => {
let mut items = Vec::new();
for item in completions {
let kind = match item.kind {
ra_ap_ide::CompletionItemKind::SymbolKind(sk) => match sk {
ra_ap_ide::SymbolKind::Function => {
crate::SymbolKind::Function
}
ra_ap_ide::SymbolKind::Struct => {
crate::SymbolKind::Struct
}
ra_ap_ide::SymbolKind::Enum => crate::SymbolKind::Enum,
ra_ap_ide::SymbolKind::Trait => crate::SymbolKind::Trait,
ra_ap_ide::SymbolKind::Module => {
crate::SymbolKind::Module
}
ra_ap_ide::SymbolKind::Const => crate::SymbolKind::Const,
ra_ap_ide::SymbolKind::Static => {
crate::SymbolKind::Static
}
ra_ap_ide::SymbolKind::TypeAlias => {
crate::SymbolKind::Type
}
_ => crate::SymbolKind::Variable,
},
_ => crate::SymbolKind::Variable,
};
items.push(crate::CompletionItem {
label: item.label.to_string(),
kind,
detail: item.detail.map(|d| d.to_string()),
documentation: item.documentation.map(|d| format!("{:?}", d)),
});
}
Ok::<Vec<crate::CompletionItem>, String>(items)
}
None => Ok::<Vec<crate::CompletionItem>, String>(Vec::new()),
}
}));
match result {
Ok(Ok(mut items)) => {
if !prefix.is_empty() {
items.retain(|item| {
item.label
.to_lowercase()
.starts_with(&prefix.to_lowercase())
});
}
if !items.is_empty() {
Ok(items)
} else {
self.base.completions(source, position)
}
}
Ok(Err(_)) | Err(_) => {
self.base.completions(source, position)
}
}
}
fn format(&self, source: &str) -> Result<String, String> {
self.base.format(source)
}
}
impl SemanticJudge for SemanticRustJudge {
fn hover(
&self,
source: &str,
position: Position,
) -> Result<Option<HoverInfo>, String> {
let (analysis, file_id) = self.create_analysis(source)?;
let offset = self
.position_to_offset(source, position)
.ok_or_else(|| "Invalid position".to_string())?;
let file_range = FileRange {
file_id,
range: ra_ap_syntax::TextRange::new(offset, offset),
};
let hover_config = HoverConfig {
links_in_hover: true,
memory_layout: None,
documentation: true,
keywords: true,
format: HoverDocFormat::Markdown,
max_trait_assoc_items_count: None,
max_fields_count: None,
max_enum_variants_count: None,
};
match analysis.hover(&hover_config, file_range) {
Ok(Some(hover_result)) => {
let range_start =
self.text_size_to_position(source, hover_result.range.start());
let range_end =
self.text_size_to_position(source, hover_result.range.end());
Ok(Some(HoverInfo {
range: Range {
start: range_start,
end: range_end,
},
contents: hover_result.info.markup.as_str().to_string(),
type_info: None,
}))
}
Ok(None) => Ok(None),
Err(e) => Err(format!("Hover analysis failed: {}", e)),
}
}
fn type_at_position(
&self,
source: &str,
position: Position,
) -> Result<Option<TypeInfo>, String> {
self.hover(source, position).map(|opt| {
opt.map(|hover| TypeInfo {
name: hover.contents.clone(),
full_name: hover.contents,
documentation: None,
})
})
}
fn find_references(
&self,
source: &str,
position: Position,
) -> Result<Vec<Reference>, String> {
let (analysis, file_id) = self.create_analysis(source)?;
let offset = self
.position_to_offset(source, position)
.ok_or_else(|| "Invalid position".to_string())?;
let file_pos = FilePosition { file_id, offset };
match analysis.find_all_refs(file_pos, None) {
Ok(Some(refs_vec)) => {
let mut references = Vec::new();
for ref_result in refs_vec {
if let Some(decl) = ref_result.declaration {
let start = self.text_size_to_position(
source,
decl.nav.focus_range.unwrap_or(decl.nav.full_range).start(),
);
let end = self.text_size_to_position(
source,
decl.nav.focus_range.unwrap_or(decl.nav.full_range).end(),
);
references.push(Reference {
range: Range { start, end },
is_definition: true,
});
}
for (_file_id, ranges) in ref_result.references {
for (range, _category) in ranges {
let start = self.text_size_to_position(source, range.start());
let end = self.text_size_to_position(source, range.end());
references.push(Reference {
range: Range { start, end },
is_definition: false,
});
}
}
}
Ok(references)
}
Ok(None) => Ok(Vec::new()),
Err(e) => Err(format!("Find references failed: {}", e)),
}
}
fn goto_definition(
&self,
source: &str,
position: Position,
) -> Result<Option<Range>, String> {
let (analysis, file_id) = self.create_analysis(source)?;
let offset = self
.position_to_offset(source, position)
.ok_or_else(|| "Invalid position".to_string())?;
let file_pos = FilePosition { file_id, offset };
match analysis.goto_definition(file_pos) {
Ok(Some(nav_targets)) => {
if let Some(target) = nav_targets.info.first() {
let start = self.text_size_to_position(
source,
target.focus_range.unwrap_or(target.full_range).start(),
);
let end = self.text_size_to_position(
source,
target.focus_range.unwrap_or(target.full_range).end(),
);
Ok(Some(Range { start, end }))
} else {
Ok(None)
}
}
Ok(None) => Ok(None),
Err(e) => Err(format!("Go to definition failed: {}", e)),
}
}
fn signature_help(
&self,
source: &str,
position: Position,
) -> Result<Option<SignatureHelp>, String> {
let (analysis, file_id) = self.create_analysis(source)?;
let offset = self
.position_to_offset(source, position)
.ok_or_else(|| "Invalid position".to_string())?;
let file_pos = FilePosition { file_id, offset };
match analysis.signature_help(file_pos) {
Ok(Some(sig_help)) => {
Ok(Some(SignatureHelp {
label: sig_help.signature.clone(),
documentation: sig_help.doc.as_ref().map(|d| d.as_str().to_string()),
parameters: Vec::new(), active_parameter: sig_help.active_parameter.map(|i| i as usize),
}))
}
Ok(None) => Ok(None),
Err(e) => Err(format!("Signature help failed: {}", e)),
}
}
fn infer_type(
&self,
source: &str,
position: Position,
) -> Result<Option<String>, String> {
self.type_at_position(source, position)
.map(|opt| opt.map(|ti| ti.name))
}
}