use std::{ffi::OsString, fs, path::Path};
use anyhow::Result;
use cgraph::{
app::{AnalysisPhase, AnalysisStatus, App},
cli::Cli,
config::ProjectConfig,
fetch::{
HierarchyClient, WorkspaceSymbolClient,
lsp::{LspConfig, LspProvider},
treesitter::{TreeSitterLanguage, TreeSitterProvider},
},
ipc::IpcServer,
tui,
};
use clap::Parser;
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
let workspace = cli.workspace.clone();
let ipc_socket = cli.ipc_socket.clone();
let project_config = ProjectConfig::load(&workspace)?;
let lsp_config = lsp_config(&cli, project_config.workspace_only);
let mut app = App::from_cli(cli);
app.set_symbol_filter(project_config.symbol_filter);
let mut ipc_server = match ipc_socket {
Some(socket_path) => Some(IpcServer::start(socket_path)?),
None => None,
};
let mut lsp = match lsp_config {
Some(config) => {
let server_name = config.program.to_string_lossy().into_owned();
match LspProvider::start(config).await {
Ok(lsp) => {
let server_name = lsp
.server_info()
.map(|info| info.name.clone())
.unwrap_or(server_name);
app.set_analysis_status(AnalysisStatus::lsp(server_name, AnalysisPhase::Ready));
Some(lsp)
}
Err(error) => {
app.set_analysis_error(format!("Failed to start LSP: {error:#}"));
let mut status = AnalysisStatus::lsp(server_name, AnalysisPhase::Error);
status.message = Some(format!("Failed to start: {error:#}"));
app.set_analysis_status(status);
None
}
}
}
None => {
app.set_analysis_error("No LSP or supported Tree-sitter provider is available");
app.set_analysis_status(AnalysisStatus::inactive(
"No LSP configured; checking Tree-sitter fallback",
));
None
}
};
let tree_sitter = if lsp.is_none() {
start_tree_sitter_fallback(&workspace, &mut app)
} else {
start_tree_sitter_hierarchy_fallback(&workspace)
};
let symbol_client = lsp
.as_ref()
.map(LspProvider::workspace_symbol_client)
.map(WorkspaceSymbolClient::from)
.or_else(|| {
tree_sitter
.as_ref()
.map(TreeSitterProvider::workspace_symbol_client)
.map(WorkspaceSymbolClient::from)
});
let hierarchy_client = match (lsp.as_ref(), tree_sitter.as_ref()) {
(Some(lsp), Some(tree_sitter)) => Some(HierarchyClient::with_fallback(
lsp.hierarchy_client(),
tree_sitter.hierarchy_client(),
)),
(Some(lsp), None) => Some(HierarchyClient::from(lsp.hierarchy_client())),
(None, Some(tree_sitter)) => Some(HierarchyClient::from(tree_sitter.hierarchy_client())),
(None, None) => None,
};
let lsp_status_receiver = lsp.as_mut().and_then(LspProvider::take_status_receiver);
let ipc_event_sender = ipc_server.as_ref().map(IpcServer::event_sender);
let ipc_command_receiver = ipc_server
.as_mut()
.and_then(IpcServer::take_command_receiver);
let mut terminal = tui::init()?;
let run_result = tui::run(
&mut terminal,
&mut app,
symbol_client,
hierarchy_client,
lsp_status_receiver,
ipc_event_sender,
ipc_command_receiver,
);
let restore_result = tui::restore(&mut terminal);
let mut result = run_result.and(restore_result);
if let Some(ipc_server) = ipc_server {
result = result.and(ipc_server.shutdown().await);
}
if let Some(lsp) = lsp {
result = result.and(lsp.shutdown().await);
}
result
}
fn start_tree_sitter_fallback(workspace: &Path, app: &mut App) -> Option<TreeSitterProvider> {
let Some(language) = TreeSitterLanguage::detect(workspace) else {
if app.analysis_status.phase != AnalysisPhase::Error {
app.set_analysis_status(AnalysisStatus::inactive(
"No LSP and no supported Tree-sitter language detected",
));
}
return None;
};
let mut initializing = AnalysisStatus::tree_sitter(language.name(), AnalysisPhase::Working);
initializing.message = Some("Initializing grammar and symbol query".to_owned());
app.set_analysis_status(initializing);
match TreeSitterProvider::start(workspace, language) {
Ok(provider) => {
let mut status = AnalysisStatus::tree_sitter(language.name(), AnalysisPhase::Ready);
status.message = Some("Syntax index builds on first search or expansion".to_owned());
app.set_analysis_status(status);
Some(provider)
}
Err(error) => {
let mut status = AnalysisStatus::tree_sitter(language.name(), AnalysisPhase::Error);
status.message = Some(format!("Grammar/query initialization failed: {error:#}"));
app.set_analysis_status(status);
None
}
}
}
fn start_tree_sitter_hierarchy_fallback(workspace: &Path) -> Option<TreeSitterProvider> {
let language = TreeSitterLanguage::detect(workspace)?;
TreeSitterProvider::start(workspace, language).ok()
}
fn lsp_config(cli: &Cli, workspace_only: bool) -> Option<LspConfig> {
if cli.no_lsp {
return None;
}
let program = cli
.lsp
.clone()
.or_else(|| detect_language_server(&cli.workspace))?;
Some(
LspConfig::for_server(program, &cli.workspace)
.workspace_only(workspace_only)
.args(cli.lsp_args.clone()),
)
}
fn detect_language_server(workspace: &Path) -> Option<OsString> {
if workspace.join("Cargo.toml").is_file() {
return Some(OsString::from("rust-analyzer"));
}
if workspace.join("compile_commands.json").is_file()
|| workspace.join("CMakeLists.txt").is_file()
|| contains_source_with_extension(workspace, &["c", "cc", "cpp", "cxx", "h", "hpp"])
{
return Some(OsString::from("clangd"));
}
if workspace.join("pyproject.toml").is_file()
|| workspace.join("pyrefly.toml").is_file()
|| workspace.join("setup.py").is_file()
|| workspace.join("requirements.txt").is_file()
|| contains_source_with_extension(workspace, &["py"])
{
return Some(OsString::from("pyrefly"));
}
None
}
fn contains_source_with_extension(workspace: &Path, extensions: &[&str]) -> bool {
fs::read_dir(workspace).is_ok_and(|entries| {
entries.filter_map(Result::ok).any(|entry| {
entry
.path()
.extension()
.and_then(|extension| extension.to_str())
.is_some_and(|extension| extensions.contains(&extension))
})
})
}
#[cfg(test)]
mod tests {
use std::{
fs,
time::{SystemTime, UNIX_EPOCH},
};
use cgraph::{
app::{AnalysisBackend, AnalysisPhase, App},
cli::Cli,
fetch::HierarchyQuery,
state::{HierarchyDirection, HierarchyKind, SourceLocation, SymbolIdentity},
};
use clap::Parser;
use super::{lsp_config, start_tree_sitter_fallback};
#[tokio::test]
async fn initializes_a_queryable_tree_sitter_fallback_and_reports_ready() {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let workspace = std::env::temp_dir().join(format!("cgraph-main-{unique}"));
fs::create_dir(&workspace).unwrap();
fs::write(
workspace.join("main.py"),
"def helper():\n pass\n\ndef main():\n helper()\n",
)
.unwrap();
let cli = Cli::try_parse_from([
"cgraph",
"--no-lsp",
"--workspace",
workspace.to_str().unwrap(),
])
.unwrap();
let mut app = App::from_cli(cli);
let provider = start_tree_sitter_fallback(&workspace, &mut app);
let provider = provider.unwrap();
assert_eq!(
app.analysis_status.backend,
AnalysisBackend::TreeSitter("Python".to_owned())
);
assert_eq!(app.analysis_status.phase, AnalysisPhase::Ready);
let symbols = provider.workspace_symbol_client().query("").await.unwrap();
let main = symbols.iter().find(|symbol| symbol.name == "main").unwrap();
let position = main.range.unwrap().start;
let response = provider
.hierarchy_client()
.query(HierarchyQuery {
symbol: SymbolIdentity {
symbol: "main".to_owned(),
kind: HierarchyKind::Call,
location: Some(SourceLocation {
uri: main.uri.to_string(),
line: Some(position.line),
character: Some(position.character),
}),
},
direction: HierarchyDirection::Outgoing,
})
.await
.unwrap();
assert_eq!(
response
.children
.iter()
.map(|child| child.symbol.as_str())
.collect::<Vec<_>>(),
["helper"]
);
fs::remove_dir_all(workspace).unwrap();
}
#[test]
fn selects_pyrefly_as_the_default_python_server_and_preserves_explicit_pylsp() {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let workspace = std::env::temp_dir().join(format!("cgraph-pyrefly-{unique}"));
fs::create_dir(&workspace).unwrap();
fs::write(workspace.join("pyrefly.toml"), "").unwrap();
let detected =
Cli::try_parse_from(["cgraph", "--workspace", workspace.to_str().unwrap()]).unwrap();
let detected = lsp_config(&detected, true).unwrap();
assert_eq!(detected.program, "pyrefly");
assert_eq!(detected.args, ["lsp"].map(std::ffi::OsString::from));
assert!(detected.workspace_only);
let explicit = Cli::try_parse_from([
"cgraph",
"--workspace",
workspace.to_str().unwrap(),
"--lsp",
"pylsp",
])
.unwrap();
let explicit = lsp_config(&explicit, true).unwrap();
assert_eq!(explicit.program, "pylsp");
assert!(explicit.args.is_empty());
fs::remove_dir_all(workspace).unwrap();
}
}