use crate::{
CompletionItem, Diagnostic, Judge, Position, Range, Severity, Symbol, SymbolKind,
};
use ra_ap_base_db::{
CrateGraph, CrateOrigin, Env, SourceDatabase, SourceDatabaseFileInputExt, SourceRoot,
SourceRootDatabase,
};
use ra_ap_ide::{Analysis, AnalysisHost, Edition as IdeEdition, FilePosition};
use ra_ap_ide_db::base_db::salsa::Durability;
use ra_ap_syntax::{
ast::{self, HasName},
AstNode, Edition, SourceFile, SyntaxKind, TextRange, TextSize,
};
use ra_ap_vfs::{file_set::FileSet, FileId, VfsPath};
use std::cell::RefCell;
use std::collections::HashMap;
use std::path::PathBuf;
use triomphe::Arc;
pub struct RustJudge {
#[allow(dead_code)]
project_root: Option<PathBuf>,
host: RefCell<AnalysisHost>,
file_ids: RefCell<HashMap<String, FileId>>,
next_file_id: RefCell<u32>,
has_workspace: bool,
}
impl RustJudge {
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 {
project_root: None,
host: RefCell::new(AnalysisHost::default()),
file_ids: RefCell::new(HashMap::new()),
next_file_id: RefCell::new(0),
has_workspace: false,
}
}
pub fn with_project(project_root: PathBuf) -> Self {
if let Ok(judge) = Self::with_workspace(&project_root) {
return judge;
}
Self {
project_root: Some(project_root),
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 {
project_root: Some(project_path.to_path_buf()),
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);
Err(format!("Failed to load workspace: {:?}", e))
}
}
}
fn text_range_to_position(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 }
}
fn text_range_to_range(source: &str, range: TextRange) -> Range {
Range {
start: Self::text_range_to_position(source, range.start()),
end: Self::text_range_to_position(source, range.end()),
}
}
pub fn extract_word_prefix(source: &str, position: Position) -> String {
let lines: Vec<&str> = source.lines().collect();
if position.line >= lines.len() {
return String::new();
}
let line = lines[position.line];
if position.column > line.len() {
return String::new();
}
let mut start = position.column;
let chars: Vec<char> = line.chars().collect();
while start > 0 {
let prev_char = chars.get(start - 1);
match prev_char {
Some(c) if c.is_alphanumeric() || *c == '_' => {
start -= 1;
}
_ => break,
}
}
chars
.iter()
.skip(start)
.take(position.column - start)
.collect()
}
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 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,
IdeEdition::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 syntax_completions(
&self,
source: &str,
position: Position,
) -> Result<Vec<CompletionItem>, String> {
let prefix = Self::extract_word_prefix(source, position);
let mut all_items = Vec::new();
if let Ok(symbols) = self.symbols(source) {
for symbol in symbols {
all_items.push(CompletionItem {
label: symbol.name.clone(),
kind: symbol.kind,
detail: symbol.detail.clone(),
documentation: Some(format!(
"Defined at line {}",
symbol.range.start.line + 1
)),
});
}
}
all_items.extend(self.get_keyword_completions());
if !prefix.is_empty() {
all_items.retain(|item| item.label.starts_with(&prefix));
}
Ok(all_items)
}
fn get_keyword_completions(&self) -> Vec<CompletionItem> {
vec![
CompletionItem {
label: "fn".to_string(),
kind: SymbolKind::Function,
detail: Some("fn name() {}".to_string()),
documentation: Some("Function declaration".to_string()),
},
CompletionItem {
label: "struct".to_string(),
kind: SymbolKind::Struct,
detail: Some("struct Name {}".to_string()),
documentation: Some("Struct declaration".to_string()),
},
CompletionItem {
label: "enum".to_string(),
kind: SymbolKind::Enum,
detail: Some("enum Name {}".to_string()),
documentation: Some("Enum declaration".to_string()),
},
CompletionItem {
label: "impl".to_string(),
kind: SymbolKind::Impl,
detail: Some("impl Type {}".to_string()),
documentation: Some("Implementation block".to_string()),
},
CompletionItem {
label: "trait".to_string(),
kind: SymbolKind::Trait,
detail: Some("trait Name {}".to_string()),
documentation: Some("Trait declaration".to_string()),
},
CompletionItem {
label: "mod".to_string(),
kind: SymbolKind::Module,
detail: Some("mod name {}".to_string()),
documentation: Some("Module declaration".to_string()),
},
CompletionItem {
label: "const".to_string(),
kind: SymbolKind::Const,
detail: Some("const NAME: Type = value;".to_string()),
documentation: Some("Constant declaration".to_string()),
},
CompletionItem {
label: "static".to_string(),
kind: SymbolKind::Static,
detail: Some("static NAME: Type = value;".to_string()),
documentation: Some("Static variable declaration".to_string()),
},
CompletionItem {
label: "type".to_string(),
kind: SymbolKind::Type,
detail: Some("type Name = Type;".to_string()),
documentation: Some("Type alias".to_string()),
},
CompletionItem {
label: "let".to_string(),
kind: SymbolKind::Variable,
detail: Some("let name = value;".to_string()),
documentation: Some("Variable binding".to_string()),
},
CompletionItem {
label: "mut".to_string(),
kind: SymbolKind::Variable,
detail: Some("mut".to_string()),
documentation: Some("Mutable binding".to_string()),
},
CompletionItem {
label: "pub".to_string(),
kind: SymbolKind::Module,
detail: Some("pub".to_string()),
documentation: Some("Public visibility".to_string()),
},
CompletionItem {
label: "use".to_string(),
kind: SymbolKind::Module,
detail: Some("use path::to::item;".to_string()),
documentation: Some("Import items".to_string()),
},
CompletionItem {
label: "if".to_string(),
kind: SymbolKind::Function,
detail: Some("if condition {}".to_string()),
documentation: Some("Conditional".to_string()),
},
CompletionItem {
label: "else".to_string(),
kind: SymbolKind::Function,
detail: Some("else {}".to_string()),
documentation: Some("Else clause".to_string()),
},
CompletionItem {
label: "match".to_string(),
kind: SymbolKind::Function,
detail: Some("match value {}".to_string()),
documentation: Some("Pattern matching".to_string()),
},
CompletionItem {
label: "for".to_string(),
kind: SymbolKind::Function,
detail: Some("for item in iter {}".to_string()),
documentation: Some("For loop".to_string()),
},
CompletionItem {
label: "while".to_string(),
kind: SymbolKind::Function,
detail: Some("while condition {}".to_string()),
documentation: Some("While loop".to_string()),
},
CompletionItem {
label: "loop".to_string(),
kind: SymbolKind::Function,
detail: Some("loop {}".to_string()),
documentation: Some("Infinite loop".to_string()),
},
CompletionItem {
label: "return".to_string(),
kind: SymbolKind::Function,
detail: Some("return value;".to_string()),
documentation: Some("Return from function".to_string()),
},
CompletionItem {
label: "println!".to_string(),
kind: SymbolKind::Function,
detail: Some("println!(...)".to_string()),
documentation: Some("Print to stdout with newline".to_string()),
},
CompletionItem {
label: "print!".to_string(),
kind: SymbolKind::Function,
detail: Some("print!(...)".to_string()),
documentation: Some("Print to stdout".to_string()),
},
CompletionItem {
label: "format!".to_string(),
kind: SymbolKind::Function,
detail: Some("format!(...)".to_string()),
documentation: Some("Format string".to_string()),
},
CompletionItem {
label: "vec!".to_string(),
kind: SymbolKind::Function,
detail: Some("vec![...]".to_string()),
documentation: Some("Create a vector".to_string()),
},
CompletionItem {
label: "assert!".to_string(),
kind: SymbolKind::Function,
detail: Some("assert!(...)".to_string()),
documentation: Some("Assert condition".to_string()),
},
CompletionItem {
label: "panic!".to_string(),
kind: SymbolKind::Function,
detail: Some("panic!(...)".to_string()),
documentation: Some("Panic with a message".to_string()),
},
CompletionItem {
label: "String".to_string(),
kind: SymbolKind::Struct,
detail: Some("String".to_string()),
documentation: Some("Heap-allocated string".to_string()),
},
CompletionItem {
label: "Vec".to_string(),
kind: SymbolKind::Struct,
detail: Some("Vec<T>".to_string()),
documentation: Some("Growable array".to_string()),
},
CompletionItem {
label: "Option".to_string(),
kind: SymbolKind::Enum,
detail: Some("Option<T>".to_string()),
documentation: Some("Optional value".to_string()),
},
CompletionItem {
label: "Result".to_string(),
kind: SymbolKind::Enum,
detail: Some("Result<T, E>".to_string()),
documentation: Some("Result of an operation".to_string()),
},
CompletionItem {
label: "Some".to_string(),
kind: SymbolKind::Enum,
detail: Some("Some(value)".to_string()),
documentation: Some("Option variant with value".to_string()),
},
CompletionItem {
label: "None".to_string(),
kind: SymbolKind::Enum,
detail: Some("None".to_string()),
documentation: Some("Option variant without value".to_string()),
},
CompletionItem {
label: "Ok".to_string(),
kind: SymbolKind::Enum,
detail: Some("Ok(value)".to_string()),
documentation: Some("Result success variant".to_string()),
},
CompletionItem {
label: "Err".to_string(),
kind: SymbolKind::Enum,
detail: Some("Err(error)".to_string()),
documentation: Some("Result error variant".to_string()),
},
CompletionItem {
label: "Self".to_string(),
kind: SymbolKind::Type,
detail: Some("Self".to_string()),
documentation: Some("Type alias for current type".to_string()),
},
CompletionItem {
label: "self".to_string(),
kind: SymbolKind::Variable,
detail: Some("self".to_string()),
documentation: Some("Current instance".to_string()),
},
]
}
}
impl Default for RustJudge {
fn default() -> Self {
Self::new()
}
}
impl Judge for RustJudge {
fn diagnostics(&self, source: &str) -> Vec<Diagnostic> {
let parse = SourceFile::parse(source, Edition::CURRENT);
let mut diagnostics = Vec::new();
for error in parse.errors() {
let range = error.range();
diagnostics.push(Diagnostic {
severity: Severity::Error,
message: error.to_string(),
range: Some(Self::text_range_to_range(source, range)),
});
}
diagnostics
}
fn symbols(&self, source: &str) -> Result<Vec<Symbol>, String> {
let parse = SourceFile::parse(source, Edition::CURRENT);
let root = parse.syntax_node();
let mut symbols = Vec::new();
for node in root.descendants() {
match node.kind() {
SyntaxKind::FN => {
if let Some(func) = ast::Fn::cast(node.clone()) {
if let Some(name) = func.name() {
let range = name.syntax().text_range();
let params = if let Some(param_list) = func.param_list() {
param_list.to_string()
} else {
"()".to_string()
};
let ret_type = if let Some(ret) = func.ret_type() {
format!(" {}", ret)
} else {
String::new()
};
symbols.push(Symbol {
kind: SymbolKind::Function,
name: name.to_string(),
range: Self::text_range_to_range(source, range),
detail: Some(format!(
"fn {}{}{}",
name, params, ret_type
)),
});
}
}
}
SyntaxKind::STRUCT => {
if let Some(s) = ast::Struct::cast(node.clone()) {
if let Some(name) = s.name() {
let range = name.syntax().text_range();
symbols.push(Symbol {
kind: SymbolKind::Struct,
name: name.to_string(),
range: Self::text_range_to_range(source, range),
detail: Some(format!("struct {}", name)),
});
}
}
}
SyntaxKind::ENUM => {
if let Some(e) = ast::Enum::cast(node.clone()) {
if let Some(name) = e.name() {
let range = name.syntax().text_range();
symbols.push(Symbol {
kind: SymbolKind::Enum,
name: name.to_string(),
range: Self::text_range_to_range(source, range),
detail: Some(format!("enum {}", name)),
});
}
}
}
SyntaxKind::TRAIT => {
if let Some(t) = ast::Trait::cast(node.clone()) {
if let Some(name) = t.name() {
let range = name.syntax().text_range();
symbols.push(Symbol {
kind: SymbolKind::Trait,
name: name.to_string(),
range: Self::text_range_to_range(source, range),
detail: Some(format!("trait {}", name)),
});
}
}
}
SyntaxKind::MODULE => {
if let Some(m) = ast::Module::cast(node.clone()) {
if let Some(name) = m.name() {
let range = name.syntax().text_range();
symbols.push(Symbol {
kind: SymbolKind::Module,
name: name.to_string(),
range: Self::text_range_to_range(source, range),
detail: Some(format!("mod {}", name)),
});
}
}
}
SyntaxKind::CONST => {
if let Some(c) = ast::Const::cast(node.clone()) {
if let Some(name) = c.name() {
let range = name.syntax().text_range();
let ty = if let Some(ty) = c.ty() {
format!(": {}", ty)
} else {
String::new()
};
symbols.push(Symbol {
kind: SymbolKind::Const,
name: name.to_string(),
range: Self::text_range_to_range(source, range),
detail: Some(format!("const {}{}", name, ty)),
});
}
}
}
SyntaxKind::STATIC => {
if let Some(s) = ast::Static::cast(node.clone()) {
if let Some(name) = s.name() {
let range = name.syntax().text_range();
let ty = if let Some(ty) = s.ty() {
format!(": {}", ty)
} else {
String::new()
};
symbols.push(Symbol {
kind: SymbolKind::Static,
name: name.to_string(),
range: Self::text_range_to_range(source, range),
detail: Some(format!("static {}{}", name, ty)),
});
}
}
}
SyntaxKind::TYPE_ALIAS => {
if let Some(t) = ast::TypeAlias::cast(node.clone()) {
if let Some(name) = t.name() {
let range = name.syntax().text_range();
let ty = if let Some(ty) = t.ty() {
format!(" = {}", ty)
} else {
String::new()
};
symbols.push(Symbol {
kind: SymbolKind::Type,
name: name.to_string(),
range: Self::text_range_to_range(source, range),
detail: Some(format!("type {}{}", name, ty)),
});
}
}
}
SyntaxKind::IMPL => {
if let Some(i) = ast::Impl::cast(node.clone()) {
if let Some(self_ty) = i.self_ty() {
let type_name = self_ty.to_string();
let range = self_ty.syntax().text_range();
let trait_name = if let Some(trait_ref) = i.trait_() {
format!("{} for ", trait_ref)
} else {
String::new()
};
symbols.push(Symbol {
kind: SymbolKind::Impl,
name: format!("impl {}{}", trait_name, type_name),
range: Self::text_range_to_range(source, range),
detail: Some(format!("impl {}{}", trait_name, type_name)),
});
}
}
}
SyntaxKind::MACRO_RULES => {
if let Some(m) = ast::MacroRules::cast(node.clone()) {
if let Some(name) = m.name() {
let range = name.syntax().text_range();
symbols.push(Symbol {
kind: SymbolKind::Macro,
name: name.to_string(),
range: Self::text_range_to_range(source, range),
detail: Some(format!("macro_rules! {}", name)),
});
}
}
}
_ => {}
}
}
Ok(symbols)
}
fn symbol_at_position(
&self,
source: &str,
position: Position,
) -> Result<Option<Symbol>, String> {
let symbols = self.symbols(source)?;
Ok(symbols.into_iter().find(|sym| {
position.line >= sym.range.start.line
&& position.line <= sym.range.end.line
&& (position.line > sym.range.start.line
|| position.column >= sym.range.start.column)
&& (position.line < sym.range.end.line
|| position.column <= sym.range.end.column)
}))
}
fn completions(
&self,
source: &str,
position: Position,
) -> Result<Vec<CompletionItem>, String> {
let prefix = Self::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 => SymbolKind::Function,
ra_ap_ide::SymbolKind::Struct => SymbolKind::Struct,
ra_ap_ide::SymbolKind::Enum => SymbolKind::Enum,
ra_ap_ide::SymbolKind::Trait => SymbolKind::Trait,
ra_ap_ide::SymbolKind::Module => SymbolKind::Module,
ra_ap_ide::SymbolKind::Const => SymbolKind::Const,
ra_ap_ide::SymbolKind::Static => SymbolKind::Static,
ra_ap_ide::SymbolKind::TypeAlias => SymbolKind::Type,
_ => SymbolKind::Variable,
},
_ => SymbolKind::Variable,
};
items.push(CompletionItem {
label: item.label.to_string(),
kind,
detail: item.detail.map(|d| d.to_string()),
documentation: item.documentation.map(|d| format!("{:?}", d)),
});
}
Ok::<Vec<CompletionItem>, String>(items)
}
None => Ok::<Vec<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.syntax_completions(source, position)
}
}
Ok(Err(_)) | Err(_) => {
self.syntax_completions(source, position)
}
}
}
fn format(&self, source: &str) -> Result<String, String> {
let parse = SourceFile::parse(source, Edition::CURRENT);
if !parse.errors().is_empty() {
return Err(format!("Syntax errors: {:?}", parse.errors()));
}
Ok(source.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_valid_rust() {
let judge = RustJudge::new();
let source = r#"
fn main() {
println!("Hello, world!");
}
"#;
let diagnostics = judge.diagnostics(source);
assert!(diagnostics.is_empty());
}
#[test]
fn test_parse_invalid_rust() {
let judge = RustJudge::new();
let source = r#"
fn main( {
println!("Hello, world!");
}
"#;
let diagnostics = judge.diagnostics(source);
assert!(!diagnostics.is_empty());
assert_eq!(diagnostics[0].severity, Severity::Error);
}
#[test]
fn test_find_symbols() {
let judge = RustJudge::new();
let source = r#"
struct Foo;
fn bar() {}
trait Baz {}
"#;
let symbols = judge.symbols(source).unwrap();
assert_eq!(symbols.len(), 3);
assert_eq!(symbols[0].kind, SymbolKind::Struct);
assert_eq!(symbols[0].name, "Foo");
assert_eq!(symbols[1].kind, SymbolKind::Function);
assert_eq!(symbols[1].name, "bar");
assert_eq!(symbols[2].kind, SymbolKind::Trait);
assert_eq!(symbols[2].name, "Baz");
}
#[test]
fn test_function_with_types() {
let judge = RustJudge::new();
let source = r#"
fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
"#;
let symbols = judge.symbols(source).unwrap();
assert_eq!(symbols.len(), 1);
assert_eq!(symbols[0].kind, SymbolKind::Function);
assert_eq!(symbols[0].name, "greet");
assert!(symbols[0].detail.as_ref().unwrap().contains("name"));
}
#[test]
fn test_struct_with_impl() {
let judge = RustJudge::new();
let source = r#"
struct Person {
name: String,
}
impl Person {
fn new(name: String) -> Self {
Self { name }
}
}
"#;
let symbols = judge.symbols(source).unwrap();
assert!(symbols
.iter()
.any(|s| s.kind == SymbolKind::Struct && s.name == "Person"));
assert!(symbols.iter().any(|s| s.kind == SymbolKind::Impl));
assert!(symbols
.iter()
.any(|s| s.kind == SymbolKind::Function && s.name == "new"));
}
}