1use std::borrow::Cow;
3use std::collections::HashSet;
4use std::env;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7use std::time::Duration;
8
9use argh::FromArgs;
10use bacon::Bacon;
11use ls_types::{ProgressToken, Uri, WorkspaceFolder};
12use native::Cargo;
13use rand::Rng;
14use tokio::sync::RwLock;
15use tokio::task::JoinHandle;
16use tokio::time::Instant;
17use tokio_util::sync::CancellationToken;
18use tower_lsp_server::{Client, LspService, Server};
19use tracing_subscriber::fmt::format::FmtSpan;
20
21mod bacon;
22mod lsp;
23mod native;
24
25const PKG_NAME: &str = env!("CARGO_PKG_NAME");
26pub const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
27const LOCATIONS_FILE: &str = ".bacon-locations";
28const BACON_BACKGROUND_COMMAND: &str = "bacon";
29const BACON_BACKGROUND_COMMAND_ARGS: &str = "--headless -j bacon-ls";
30const CARGO_COMMAND_ARGS: &str =
31 "clippy --tests --all-features --all-targets --message-format json-diagnostic-rendered-ansi";
32
33#[derive(Debug, FromArgs)]
35pub struct Args {
36 #[argh(switch, short = 'v')]
38 pub version: bool,
39}
40
41#[derive(Debug, Clone, Copy)]
42enum Backend {
43 Bacon,
44 Cargo,
45}
46
47#[derive(Debug)]
48struct State {
49 project_root: Option<PathBuf>,
50 workspace_folders: Option<Vec<WorkspaceFolder>>,
51 locations_file: String,
52 update_on_save: bool,
53 update_on_save_wait_millis: Duration,
54 update_on_change: bool,
55 update_on_change_cooldown_millis: Duration,
56 validate_bacon_preferences: bool,
57 run_bacon_in_background: bool,
58 run_bacon_in_background_command: String,
59 run_bacon_in_background_command_args: String,
60 create_bacon_preferences_file: bool,
61 bacon_command_handle: Option<JoinHandle<()>>,
62 syncronize_all_open_files_wait_millis: Duration,
63 diagnostics_data_supported: bool,
64 open_files: HashSet<Uri>,
65 cancel_token: CancellationToken,
66 sync_files_handle: Option<JoinHandle<()>>,
67 backend: Backend,
68 diagnostics_version: i32,
69 cargo_command_args: String,
70 cargo_env: Vec<String>,
71 build_folder: PathBuf,
72 last_change: Instant,
73}
74
75impl Default for State {
76 fn default() -> Self {
77 Self {
78 project_root: None,
79 workspace_folders: None,
80 locations_file: LOCATIONS_FILE.to_string(),
81 update_on_save: true,
82 update_on_save_wait_millis: Duration::from_millis(1000),
83 update_on_change: false,
84 update_on_change_cooldown_millis: Duration::from_millis(5000),
85 validate_bacon_preferences: true,
86 run_bacon_in_background: true,
87 run_bacon_in_background_command: BACON_BACKGROUND_COMMAND.to_string(),
88 run_bacon_in_background_command_args: BACON_BACKGROUND_COMMAND_ARGS.to_string(),
89 create_bacon_preferences_file: true,
90 bacon_command_handle: None,
91 syncronize_all_open_files_wait_millis: Duration::from_millis(2000),
92 diagnostics_data_supported: false,
93 open_files: HashSet::new(),
94 cancel_token: CancellationToken::new(),
95 sync_files_handle: None,
96 backend: Backend::Cargo,
97 diagnostics_version: 0,
98 cargo_command_args: CARGO_COMMAND_ARGS.to_string(),
99 cargo_env: vec![],
100 build_folder: tempfile::tempdir().unwrap().path().into(),
101 last_change: Instant::now(),
102 }
103 }
104}
105
106#[derive(Debug, serde::Serialize, serde::Deserialize)]
107struct DiagnosticData<'c> {
108 corrections: Vec<Cow<'c, str>>,
109}
110
111#[derive(Debug, Default)]
112pub struct BaconLs {
113 client: Option<Arc<Client>>,
114 state: Arc<RwLock<State>>,
115}
116
117impl BaconLs {
118 fn new(client: Client) -> Self {
119 Self {
120 client: Some(Arc::new(client)),
121 state: Arc::new(RwLock::new(State::default())),
122 }
123 }
124
125 fn configure_tracing(log_level: Option<String>) {
126 let level = log_level.unwrap_or_else(|| env::var("RUST_LOG").unwrap_or("off".to_string()));
128 if level != "off" {
129 tracing_subscriber::fmt()
130 .with_env_filter(level)
131 .with_writer(
132 std::fs::OpenOptions::new()
133 .create(true)
134 .write(true)
135 .truncate(true)
136 .open(format!("{PKG_NAME}.log"))
137 .unwrap(),
138 )
139 .with_thread_names(true)
140 .with_span_events(FmtSpan::CLOSE)
141 .with_target(true)
142 .with_file(true)
143 .with_line_number(true)
144 .init();
145 }
146 }
147
148 pub async fn serve() {
150 Self::configure_tracing(None);
151 let stdin = tokio::io::stdin();
153 let stdout = tokio::io::stdout();
154 let (service, socket) = LspService::new(Self::new);
156 Server::new(stdin, stdout, socket).serve(service).await;
157 }
158
159 async fn find_git_root_directory(path: &Path) -> Option<PathBuf> {
160 let output = tokio::process::Command::new("git")
161 .arg("-C")
162 .arg(path)
163 .arg("rev-parse")
164 .arg("--show-toplevel")
165 .output()
166 .await
167 .ok()?;
168
169 if output.status.success() {
170 String::from_utf8(output.stdout).ok().map(|v| PathBuf::from(v.trim()))
171 } else {
172 None
173 }
174 }
175
176 async fn publish_diagnostics(&self, uri: &Uri) {
177 let mut guard = self.state.write().await;
178 let locations_file_name = guard.locations_file.clone();
179 let workspace_folders = guard.workspace_folders.clone();
180 let open_files = guard.open_files.clone();
181 let backend = guard.backend;
182 let command_args = guard.cargo_command_args.clone();
183 let cargo_env = guard.cargo_env.clone();
184 let project_root = guard.project_root.clone();
185 let build_folder = guard.build_folder.clone();
186 guard.diagnostics_version += 1;
187 let version = guard.diagnostics_version;
188 drop(guard);
189
190 tracing::info!(uri = uri.to_string(), "publish diagnostics");
191 match backend {
192 Backend::Bacon => {
193 Bacon::publish_diagnostics(
194 self.client.as_ref(),
195 uri,
196 &locations_file_name,
197 workspace_folders.as_deref(),
198 )
199 .await;
200 }
201 Backend::Cargo => {
202 if let Some(client) = self.client.as_ref() {
203 let token = ProgressToken::Number(rand::rng().random::<i32>());
204 let first_arg = command_args.split_whitespace().next().unwrap_or("check");
205 let progress = client
206 .progress(token, "running:")
207 .with_message(format!("cargo {first_arg}"))
208 .with_percentage(0)
209 .begin()
210 .await;
211 let diagnostics =
212 Cargo::cargo_diagnostics(&command_args, &cargo_env, project_root.as_ref(), &build_folder)
213 .await
214 .inspect_err(|err| tracing::error!(?err, "error building diagnostics"))
215 .unwrap_or_default();
216 progress.report(90).await;
217 if !diagnostics.contains_key(uri) {
218 tracing::info!(
219 uri = uri.to_string(),
220 "cleaned up cargo diagnostics. does not contain key."
221 );
222 client.publish_diagnostics(uri.clone(), vec![], Some(version)).await;
223 }
224 for (uri, diagnostics) in diagnostics.into_iter() {
225 if diagnostics.is_empty() {
226 tracing::info!(uri = uri.to_string(), "cleaned up cargo diagnostics. empty.");
227 client.publish_diagnostics(uri, vec![], Some(version)).await;
228 } else if open_files.contains(&uri) {
229 tracing::info!(uri = uri.to_string(), "sent {} cargo diagnostics", diagnostics.len());
230 client.publish_diagnostics(uri, diagnostics, Some(version)).await;
231 }
232 }
233
234 progress.report(100).await;
235 progress.finish().await;
236 }
237 }
238 }
239 }
240
241 async fn syncronize_diagnostics(state: Arc<RwLock<State>>, client: Option<Arc<Client>>) {
242 let backend = state.read().await.backend;
243 if let Backend::Bacon = backend {
244 Bacon::syncronize_diagnostics(state, client).await;
245 }
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn test_can_configure_tracing() {
255 BaconLs::configure_tracing(Some("info".to_string()));
256 }
257}