1use std::num::NonZeroU32;
42use std::panic::RefUnwindSafe;
43use std::path::PathBuf;
44use std::process::ExitCode;
45use std::time::{Duration, SystemTime};
46use std::{io, panic};
47
48use anyhow::Result;
49use cairo_lang_filesystem::db::FilesGroup;
50use cairo_lang_filesystem::ids::FileLongId;
51use cairo_lang_semantic::plugin::PluginSuite;
52use crossbeam::channel::{Receiver, select_biased};
53use governor::{Quota, RateLimiter};
54use lsp_server::Message;
55use lsp_types::RegistrationParams;
56use tracing::{debug, error, info};
57
58use crate::lang::lsp::LsProtoGroup;
59use crate::lang::proc_macros::controller::ProcMacroChannels;
60use crate::lsp::capabilities::server::{
61 collect_dynamic_registrations, collect_server_capabilities,
62};
63use crate::lsp::result::LSPResult;
64use crate::project::{ProjectController, ProjectUpdate};
65use crate::server::client::{Notifier, Requester, Responder};
66use crate::server::connection::{Connection, ConnectionInitializer};
67use crate::server::panic::is_cancelled;
68use crate::server::schedule::thread::JoinHandle;
69use crate::server::schedule::{Scheduler, Task, event_loop_thread};
70use crate::state::State;
71
72mod config;
73mod env_config;
74mod ide;
75mod lang;
76pub mod lsp;
77mod project;
78mod server;
79mod state;
80mod toolchain;
81
82#[non_exhaustive]
88#[derive(Default, Clone)]
89pub struct Tricks {
90 pub extra_plugin_suites:
93 Option<&'static (dyn Fn() -> Vec<PluginSuite> + Send + Sync + RefUnwindSafe)>,
94}
95
96pub fn start() -> ExitCode {
102 start_with_tricks(Tricks::default())
103}
104
105pub fn start_with_tricks(tricks: Tricks) -> ExitCode {
111 let _log_guard = init_logging();
112 set_panic_hook();
113
114 info!("language server starting");
115 env_config::report_to_logs();
116
117 let exit_code = match Backend::new(tricks) {
118 Ok(backend) => {
119 if let Err(err) = backend.run().map(|handle| handle.join()) {
120 error!("language server encountered an unrecoverable error: {err}");
121 ExitCode::from(1)
122 } else {
123 ExitCode::from(0)
124 }
125 }
126 Err(err) => {
127 error!("language server failed during initialization: {err}");
128 ExitCode::from(1)
129 }
130 };
131
132 info!("language server stopped");
133 exit_code
134}
135
136#[cfg(feature = "testing")]
138pub fn build_service_for_e2e_tests()
139-> (Box<dyn FnOnce() -> BackendForTesting + Send>, lsp_server::Connection) {
140 BackendForTesting::new_for_testing(Default::default())
141}
142
143fn init_logging() -> Option<impl Drop> {
147 use std::fs;
148 use std::io::IsTerminal;
149
150 use tracing_chrome::ChromeLayerBuilder;
151 use tracing_subscriber::filter::{EnvFilter, LevelFilter, Targets};
152 use tracing_subscriber::fmt::Layer;
153 use tracing_subscriber::fmt::time::Uptime;
154 use tracing_subscriber::prelude::*;
155
156 let mut guard = None;
157
158 let fmt_layer = Layer::new()
159 .with_writer(io::stderr)
160 .with_timer(Uptime::default())
161 .with_ansi(io::stderr().is_terminal())
162 .with_filter(
163 EnvFilter::builder()
164 .with_default_directive(LevelFilter::WARN.into())
165 .with_env_var(env_config::CAIRO_LS_LOG)
166 .from_env_lossy(),
167 );
168
169 let profile_layer = if env_config::tracing_profile() {
170 let mut path = PathBuf::from(format!(
171 "./cairols-profile-{}.json",
172 SystemTime::UNIX_EPOCH.elapsed().unwrap().as_micros()
173 ));
174
175 let profile_file = fs::File::create(&path).expect("Failed to create profile file.");
177
178 if let Ok(canonical) = fs::canonicalize(&path) {
180 path = canonical;
181 }
182
183 eprintln!("this LS run will output tracing profile to: {}", path.display());
184 eprintln!(
185 "open that file with https://ui.perfetto.dev (or chrome://tracing) to analyze it"
186 );
187
188 let (profile_layer, profile_layer_guard) =
189 ChromeLayerBuilder::new().writer(profile_file).include_args(true).build();
190
191 let profile_layer = profile_layer.with_filter(
194 Targets::new().with_default(LevelFilter::TRACE).with_target("salsa", LevelFilter::WARN),
195 );
196
197 guard = Some(profile_layer_guard);
198 Some(profile_layer)
199 } else {
200 None
201 };
202
203 tracing::subscriber::set_global_default(
204 tracing_subscriber::registry().with(fmt_layer).with(profile_layer),
205 )
206 .expect("Could not set up global logger.");
207
208 guard
209}
210
211fn set_panic_hook() {
213 let previous_hook = panic::take_hook();
214 panic::set_hook(Box::new(move |info| {
215 if !is_cancelled(info.payload()) {
216 previous_hook(info);
217 }
218 }))
219}
220
221struct Backend {
222 connection: Connection,
223 state: State,
224}
225
226#[cfg(feature = "testing")]
227pub struct BackendForTesting(Backend);
228
229#[cfg(feature = "testing")]
230impl BackendForTesting {
231 fn new_for_testing(
232 tricks: Tricks,
233 ) -> (Box<dyn FnOnce() -> BackendForTesting + Send>, lsp_server::Connection) {
234 let (connection_initializer, client) = ConnectionInitializer::memory();
235
236 let init = Box::new(|| {
237 BackendForTesting(Backend::initialize(tricks, connection_initializer).unwrap())
238 });
239
240 (init, client)
241 }
242
243 pub fn run_for_tests(self) -> Result<JoinHandle<Result<()>>> {
244 self.0.run()
245 }
246}
247
248impl Backend {
249 fn new(tricks: Tricks) -> Result<Self> {
250 let connection_initializer = ConnectionInitializer::stdio();
251
252 Self::initialize(tricks, connection_initializer)
253 }
254
255 fn initialize(tricks: Tricks, connection_initializer: ConnectionInitializer) -> Result<Self> {
259 let (id, init_params) = connection_initializer.initialize_start()?;
260
261 let client_capabilities = init_params.capabilities;
262 let server_capabilities = collect_server_capabilities(&client_capabilities);
263
264 let connection = connection_initializer.initialize_finish(id, server_capabilities)?;
265 let state = State::new(connection.make_sender(), client_capabilities, tricks);
266
267 Ok(Self { connection, state })
268 }
269
270 fn run(self) -> Result<JoinHandle<Result<()>>> {
272 event_loop_thread(move || {
273 let Self { mut state, connection } = self;
274 let proc_macro_channels = state.proc_macro_controller.channels();
275 let project_updates_receiver = state.project_controller.response_receiver();
276
277 let mut scheduler = Scheduler::new(&mut state, connection.make_sender());
278
279 Self::dispatch_setup_tasks(&mut scheduler);
280
281 scheduler.on_sync_task(Self::maybe_swap_database);
285
286 scheduler.on_sync_task(Self::refresh_diagnostics);
290
291 let result = Self::event_loop(
292 &connection,
293 proc_macro_channels,
294 project_updates_receiver,
295 scheduler,
296 );
297
298 state.db.cancel_all();
299
300 if let Err(err) = connection.close() {
301 error!("failed to close connection to the language server: {err:?}");
302 }
303
304 result
305 })
306 }
307
308 fn dispatch_setup_tasks(scheduler: &mut Scheduler<'_>) {
310 scheduler.local(Self::register_dynamic_capabilities);
311
312 scheduler.local(|state, _notifier, requester, _responder| {
313 let _ = state.config.reload(requester, &state.client_capabilities);
314 });
315 }
316
317 fn register_dynamic_capabilities(
318 state: &mut State,
319 _notifier: Notifier,
320 requester: &mut Requester<'_>,
321 _responder: Responder,
322 ) {
323 let registrations = collect_dynamic_registrations(&state.client_capabilities);
324
325 let _ = requester
326 .request::<lsp_types::request::RegisterCapability>(
327 RegistrationParams { registrations },
328 |()| {
329 debug!("configuration file watcher successfully registered");
330 Task::nothing()
331 },
332 )
333 .inspect_err(|e| {
334 error!(
335 "failed to register dynamic capabilities, some features may not work \
336 properly: {e:?}"
337 )
338 });
339 }
340
341 fn event_loop(
348 connection: &Connection,
349 proc_macro_channels: ProcMacroChannels,
350 project_updates_receiver: Receiver<ProjectUpdate>,
351 mut scheduler: Scheduler<'_>,
352 ) -> Result<()> {
353 let incoming = connection.incoming();
354
355 let response_resolving_limiter =
356 RateLimiter::direct(Quota::with_period(Duration::from_secs(3)).unwrap().allow_burst(
357 NonZeroU32::new(1).unwrap(),
359 ));
360
361 loop {
362 select_biased! {
363 recv(project_updates_receiver) -> project_update => {
367 let Ok(project_update) = project_update else { break };
368
369 scheduler.local(move |state, notifier, _, _| ProjectController::handle_update(state, notifier, project_update));
370 }
371 recv(incoming) -> msg => {
372 let Ok(msg) = msg else { break };
373
374 if connection.handle_shutdown(&msg)? {
375 break;
376 }
377 let task = match msg {
378 Message::Request(req) => server::request(req),
379 Message::Notification(notification) => server::notification(notification),
380 Message::Response(response) => scheduler.response(response),
381 };
382 scheduler.dispatch(task);
383 }
384 recv(proc_macro_channels.response_receiver) -> response => {
385 let Ok(()) = response else { break };
386
387 if response_resolving_limiter.check().is_ok() {
388 scheduler.local(Self::on_proc_macro_response);
389 } else {
390 let _ = proc_macro_channels.response_sender.try_send(());
391 }
392 }
393 recv(proc_macro_channels.error_receiver) -> error => {
394 let Ok(()) = error else { break };
395
396 scheduler.local(Self::on_proc_macro_error);
397 }
398 }
399 }
400
401 Ok(())
402 }
403
404 fn on_proc_macro_error(state: &mut State, _: Notifier, _: &mut Requester<'_>, _: Responder) {
407 state.proc_macro_controller.handle_error(&mut state.db, &state.config);
408 }
409
410 fn on_proc_macro_response(state: &mut State, _: Notifier, _: &mut Requester<'_>, _: Responder) {
413 state.proc_macro_controller.on_response(&mut state.db, &state.config);
414 }
415
416 fn maybe_swap_database(state: &mut State, _notifier: Notifier) {
418 state.db_swapper.maybe_swap(
419 &mut state.db,
420 &state.open_files,
421 &state.tricks,
422 &mut state.project_controller,
423 );
424 }
425
426 fn refresh_diagnostics(state: &mut State, _notifier: Notifier) {
428 state.diagnostics_controller.refresh(state);
429 }
430
431 fn reload(state: &mut State, requester: &mut Requester<'_>) -> LSPResult<()> {
433 state.project_controller.clear_loaded_workspaces();
434 state.config.reload(requester, &state.client_capabilities)?;
435
436 for uri in state.open_files.iter() {
437 let Some(file_id) = state.db.file_for_url(uri) else { continue };
438 if let FileLongId::OnDisk(file_path) = state.db.lookup_intern_file(file_id) {
439 state.project_controller.request_updating_project_for_file(file_path);
440 }
441 }
442
443 Ok(())
444 }
445}