Skip to main content

cairo_language_server/
lib.rs

1//! # CairoLS
2//!
3//! Implements the LSP protocol over stdin/out.
4//!
5//! ## Running vanilla
6//!
7//! This is basically the source code of the `cairo-language-server` and
8//! `scarb cairo-language-server` binaries.
9//!
10//! ```no_run
11//! # #![allow(clippy::needless_doctest_main)]
12//! fn main() {
13//!     cairo_language_server::start();
14//! }
15//! ```
16
17use std::path::PathBuf;
18use std::process::ExitCode;
19use std::time::SystemTime;
20use std::{io, panic};
21
22use anyhow::Result;
23use cairo_lang_filesystem::ids::FileLongId;
24use crossbeam::channel::{self, Receiver, select_biased};
25use lsp_server::Message;
26use lsp_types::RegistrationParams;
27use lsp_types::request::SemanticTokensRefresh;
28use mimalloc::MiMalloc;
29use tracing::{debug, error, info};
30
31use crate::ide::analysis_progress::AnalysisStatus;
32use crate::ide::code_lens::CodeLensController;
33use crate::lang::lsp::LsProtoGroup;
34use crate::lang::proc_macros;
35use crate::lang::proc_macros::client::ServerStatus;
36use crate::lang::proc_macros::controller::ProcMacroChannels;
37use crate::lang::proc_macros::db::ProcMacroGroup;
38use crate::lsp::capabilities::client::ClientCapabilitiesExt;
39use crate::lsp::capabilities::server::{
40    collect_dynamic_registrations, collect_server_capabilities,
41};
42use crate::lsp::result::LSPResult;
43use crate::project::{ProjectController, ProjectUpdate};
44use crate::server::client::{Notifier, Requester, Responder};
45use crate::server::connection::{Connection, ConnectionInitializer};
46use crate::server::panic::is_cancelled;
47use crate::server::schedule::thread::JoinHandle;
48use crate::server::schedule::{Scheduler, Task, event_loop_thread};
49use crate::state::{MetaState, State};
50
51mod config;
52mod env_config;
53mod ide;
54mod lang;
55pub mod lsp;
56mod project;
57mod server;
58mod state;
59#[cfg(feature = "testing")]
60pub mod testing;
61mod toolchain;
62
63#[global_allocator]
64static GLOBAL: MiMalloc = MiMalloc;
65
66/// Starts the language server.
67///
68/// See [the top-level documentation][lib] documentation for usage examples.
69///
70/// [lib]: crate#running-vanilla
71pub fn start() -> ExitCode {
72    // Return freed pages to OS immediately. mimalloc reads this env var on first allocation,
73    // so it must be set before any significant work begins.
74    unsafe { std::env::set_var("MIMALLOC_PURGE_DELAY", "0") };
75
76    let _log_guard = init_logging();
77    set_panic_hook();
78
79    info!("language server starting");
80    env_config::report_to_logs();
81
82    let exit_code = match Backend::new() {
83        Ok(backend) => {
84            if let Err(err) = backend.run().map(|handle| handle.join()) {
85                error!("language server encountered an unrecoverable error: {err}");
86                ExitCode::from(1)
87            } else {
88                ExitCode::from(0)
89            }
90        }
91        Err(err) => {
92            error!("language server failed during initialization: {err}");
93            ExitCode::from(1)
94        }
95    };
96
97    info!("language server stopped");
98    exit_code
99}
100
101/// Initialize logging infrastructure for the language server.
102///
103/// Returns a guard that should be dropped when the LS ends, to flush log files.
104fn init_logging() -> Option<impl Drop> {
105    use std::fs;
106    use std::io::IsTerminal;
107
108    use tracing_chrome::ChromeLayerBuilder;
109    use tracing_subscriber::filter::{EnvFilter, LevelFilter, Targets};
110    use tracing_subscriber::fmt::Layer;
111    use tracing_subscriber::fmt::time::Uptime;
112    use tracing_subscriber::prelude::*;
113
114    let mut guard = None;
115
116    let fmt_layer = Layer::new()
117        .with_writer(io::stderr)
118        .with_timer(Uptime::default())
119        .with_ansi(io::stderr().is_terminal())
120        .with_filter(
121            EnvFilter::builder()
122                .with_default_directive(LevelFilter::WARN.into())
123                .with_env_var(env_config::CAIRO_LS_LOG)
124                .from_env_lossy(),
125        );
126
127    let profile_layer = if env_config::tracing_profile() {
128        let mut path = PathBuf::from(format!(
129            "./cairols-profile-{}.json",
130            SystemTime::UNIX_EPOCH.elapsed().unwrap().as_micros()
131        ));
132
133        // Create the file now, so that we early panic, and `fs::canonicalize` will work.
134        let profile_file = fs::File::create(&path).expect("Failed to create profile file.");
135
136        // Try to canonicalize the path, so that it's easier to find the file from logs.
137        if let Ok(canonical) = fs::canonicalize(&path) {
138            path = canonical;
139        }
140
141        eprintln!("this LS run will output tracing profile to: {}", path.display());
142        eprintln!(
143            "open that file with https://ui.perfetto.dev (or chrome://tracing) to analyze it"
144        );
145
146        let (profile_layer, profile_layer_guard) =
147            ChromeLayerBuilder::new().writer(profile_file).include_args(true).build();
148
149        // Filter out less important Salsa logs because they are too verbose,
150        // and with them the profile file quickly grows to several GBs of data.
151        let profile_layer = profile_layer.with_filter(
152            Targets::new().with_default(LevelFilter::TRACE).with_target("salsa", LevelFilter::WARN),
153        );
154
155        guard = Some(profile_layer_guard);
156        Some(profile_layer)
157    } else {
158        None
159    };
160
161    tracing::subscriber::set_global_default(
162        tracing_subscriber::registry().with(fmt_layer).with(profile_layer),
163    )
164    .expect("Could not set up global logger.");
165
166    guard
167}
168
169/// Sets a special panic hook that skips execution for Salsa cancellation panics.
170fn set_panic_hook() {
171    let previous_hook = panic::take_hook();
172    panic::set_hook(Box::new(move |info| {
173        if !is_cancelled(info.payload()) {
174            previous_hook(info);
175        }
176    }))
177}
178
179struct Backend {
180    connection: Connection,
181    state: State,
182}
183
184impl Backend {
185    fn new() -> Result<Self> {
186        let connection_initializer = ConnectionInitializer::stdio();
187
188        Self::initialize(connection_initializer, std::env::current_dir()?)
189    }
190
191    /// Initializes the connection and crate a ready to run [`Backend`] instance.
192    ///
193    /// As part of the initialization flow, this function exchanges client and server capabilities.
194    fn initialize(connection_initializer: ConnectionInitializer, cwd: PathBuf) -> Result<Self> {
195        let (id, init_params) = connection_initializer.initialize_start()?;
196
197        let client_capabilities = init_params.capabilities;
198        let server_capabilities = collect_server_capabilities(&client_capabilities);
199
200        let connection = connection_initializer.initialize_finish(id, server_capabilities)?;
201        let state = State::new(connection.make_sender(), client_capabilities, cwd);
202
203        Ok(Self { connection, state })
204    }
205
206    /// Runs the main event loop thread and wait for its completion.
207    fn run(self) -> Result<JoinHandle<Result<()>>> {
208        event_loop_thread(move || {
209            let Self { mut state, connection } = self;
210            let proc_macro_channels = state.proc_macro_controller.channels();
211            let project_updates_receiver = state.project_controller.response_receiver();
212            let analysis_progress_receiver =
213                state.analysis_progress_controller.get_status_receiver();
214            let code_lens_request_refresh_receiver =
215                state.code_lens_controller.request_refresh_receiver();
216
217            let mut scheduler = Scheduler::new(&mut state, connection.make_sender());
218
219            Self::dispatch_setup_tasks(&mut scheduler);
220
221            // Notify the swapper about state mutation.
222            scheduler.on_sync_mut_task(Self::register_mutation_in_swapper);
223
224            // Attempt to swap the database to reduce memory use.
225            // Because diagnostics are always refreshed afterwards, the fresh database state will
226            // be quickly repopulated.
227            scheduler.on_sync_mut_task(Self::maybe_swap_database);
228
229            // Refresh diagnostics each time state changes.
230            // Although it is possible to mutate state without affecting the analysis database,
231            // we basically never hit such a case in CairoLS in happy paths.
232            scheduler.on_sync_mut_task(Self::refresh_diagnostics);
233
234            // Keep it last, marks that db mutation might happened.
235            scheduler.on_sync_mut_task(|state, _, _| {
236                state.analysis_progress_controller.mutation();
237            });
238
239            let inactivity_swap_request_receiver = scheduler
240                .meta_state
241                .lock()
242                .expect("should be able to acquire the MetaState")
243                .inactivity_monitor
244                .swap_request_receiver();
245
246            let result = Self::event_loop(
247                &connection,
248                proc_macro_channels,
249                project_updates_receiver,
250                analysis_progress_receiver,
251                code_lens_request_refresh_receiver,
252                inactivity_swap_request_receiver,
253                scheduler,
254            );
255
256            state.db.cancel_all();
257
258            if let Err(err) = connection.close() {
259                error!("failed to close connection to the language server: {err:?}");
260            }
261
262            result
263        })
264    }
265
266    /// Runs various setup tasks before entering the main event loop.
267    fn dispatch_setup_tasks(scheduler: &mut Scheduler<'_>) {
268        scheduler.local_mut(Self::register_dynamic_capabilities);
269
270        scheduler.local_mut(|state, _notifier, requester, _responder| {
271            let _ = state.config.reload_on_start(
272                requester,
273                &mut state.db,
274                &mut state.proc_macro_controller,
275                &mut state.analysis_progress_controller,
276                &state.client_capabilities,
277            );
278        });
279    }
280
281    fn register_dynamic_capabilities(
282        state: &mut State,
283        _notifier: Notifier,
284        requester: &mut Requester<'_>,
285        _responder: Responder,
286    ) {
287        let registrations = collect_dynamic_registrations(&state.client_capabilities);
288
289        let _ = requester
290            .request::<lsp_types::request::RegisterCapability>(
291                RegistrationParams { registrations },
292                |()| {
293                    debug!("capabilities successfully registered dynamically");
294                    Task::nothing()
295                },
296            )
297            .inspect_err(|e| {
298                error!(
299                    "failed to register dynamic capabilities, some features may not work \
300                     properly: {e:?}"
301                )
302            });
303    }
304
305    // +--------------------------------------------------+
306    // | Function code adopted from:                      |
307    // | Repository: https://github.com/astral-sh/ruff    |
308    // | File: `crates/ruff_server/src/server.rs`         |
309    // | Commit: 46a457318d8d259376a2b458b3f814b9b795fe69 |
310    // +--------------------------------------------------+
311    fn event_loop(
312        connection: &Connection,
313        proc_macro_channels: ProcMacroChannels,
314        project_updates_receiver: Receiver<ProjectUpdate>,
315        analysis_progress_status_receiver: Receiver<AnalysisStatus>,
316        code_lens_request_refresh_receiver: Receiver<()>,
317        inactivity_swap_request_receiver: Receiver<()>,
318        mut scheduler: Scheduler<'_>,
319    ) -> Result<()> {
320        let incoming = connection.incoming();
321        let (retry_sender, retry_receiver) = channel::unbounded();
322
323        loop {
324            select_biased! {
325                // Project updates may significantly change the state, therefore
326                // they should be handled first in case of multiple operations being ready at once.
327                // To ensure it, keep project updates channel in the first arm of `select_biased!`.
328                recv(project_updates_receiver) -> project_update => {
329                    let Ok(project_update) = project_update else { break };
330
331                    scheduler.local_mut(move |state, notifier, _, _| ProjectController::handle_update(state, notifier, project_update));
332                }
333                recv(incoming) -> msg => {
334                    let Ok(msg) = msg else { break };
335
336                    if connection.handle_shutdown(&msg)? {
337                        break;
338                    }
339                    scheduler.meta_state
340                        .lock()
341                        .expect("should be able to acquire the MetaState")
342                        .inactivity_monitor
343                        .notify_activity();
344                    let task = match msg {
345                        Message::Request(req) => server::request(req, retry_sender.clone()),
346                        Message::Notification(notification) => server::notification(notification),
347                        Message::Response(response) => scheduler.response(response),
348                    };
349                    scheduler.dispatch(task);
350                }
351                recv(proc_macro_channels.poll_responses_receiver) -> response => {
352                    let Ok(()) = response else { break };
353
354                    scheduler.local_mut(Self::on_proc_macro_response);
355                }
356                recv(proc_macro_channels.error_receiver) -> error => {
357                    let Ok(()) = error else { break };
358
359                    scheduler.local_mut(Self::on_proc_macro_error);
360                }
361                recv(retry_receiver) -> retry => {
362                    let Ok((retry_info, handler)) = retry else { break };
363
364                    let task = retry_info.task(handler);
365                    scheduler.dispatch(task);
366                }
367                recv(analysis_progress_status_receiver) -> analysis_progress_status => {
368                    let Ok(analysis_status) = analysis_progress_status else { break };
369
370                    match analysis_status {
371                        AnalysisStatus::Started => {
372                            let mut ms = scheduler.meta_state
373                                .lock()
374                                .expect("should be able to acquire the MetaState");
375                            ms.db_swapper.start_stopwatch();
376                        }
377                        AnalysisStatus::Finished => {
378                            scheduler.meta_state
379                                .lock()
380                                .expect("should be able to acquire the MetaState")
381                                .db_swapper
382                                .stop_stopwatch();
383
384                            scheduler.local(|state, _, _notifier, requester, _responder|
385                                Self::on_stopped_analysis(state, requester)
386                            );
387                        }
388                    };
389                }
390                recv(code_lens_request_refresh_receiver) -> error => {
391                    let Ok(()) = error else { break };
392
393                    scheduler.local(|state: &State, _, _, requester, _| {
394                        if state.client_capabilities.workspace_code_lens_refresh_support() {
395                            CodeLensController::handle_refresh(requester);
396                        }
397                    });
398                }
399                recv(inactivity_swap_request_receiver) -> result => {
400                    let Ok(()) = result else { break };
401
402                    let meta_state = scheduler.meta_state.clone();
403                    scheduler.local_mut(move |state, _, _, _| {
404                        let mut ms = meta_state
405                            .lock()
406                            .expect("should be able to acquire the MetaState");
407                        ms.inactivity_monitor.notify_swap_triggered();
408                        ms.db_swapper.swap_on_inactivity(
409                            &mut state.db,
410                            &state.open_files,
411                        );
412                    });
413                }
414            }
415        }
416
417        Ok(())
418    }
419
420    /// Calls [`lang::proc_macros::controller::ProcMacroClientController::handle_error`] to do its
421    /// work.
422    fn on_proc_macro_error(state: &mut State, _: Notifier, _: &mut Requester<'_>, _: Responder) {
423        state.proc_macro_controller.force_restart(&mut state.db, &state.config);
424    }
425
426    /// Calls [`lang::proc_macros::controller::ProcMacroClientController::on_response`] to do its
427    /// work.
428    fn on_proc_macro_response(
429        state: &mut State,
430        _: Notifier,
431        requester: &mut Requester<'_>,
432        _: Responder,
433    ) {
434        // Check for responses without keeping the Arc alive past this block.
435        // The Arc must be dropped before calling `handle_response`, because `handle_response` may
436        // call `force_restart` which calls `Arc::try_unwrap` and panics if any extra strong
437        // references exist.
438        let has_responses = match state.db.proc_macro_input().proc_macro_server_status(&state.db) {
439            ServerStatus::Connected(client) => client.available_responses().len() != 0,
440            _ => false,
441        };
442
443        if has_responses {
444            state.proc_macro_controller.handle_response(
445                &mut state.db,
446                &state.config,
447                &state.client_capabilities,
448                requester,
449            );
450        }
451    }
452
453    fn on_stopped_analysis(state: &State, requester: &mut Requester<'_>) {
454        proc_macros::cache::save_proc_macro_cache(&state.db);
455        state
456            .code_lens_controller
457            .schedule_refreshing_all_lenses(state.db.clone(), state.config.clone());
458
459        if state.client_capabilities.workspace_semantic_tokens_refresh_support()
460            && let Err(err) = requester.request::<SemanticTokensRefresh>((), |_| Task::nothing())
461        {
462            error!("semantic tokens refresh failed: {err:#?}");
463        }
464    }
465
466    fn register_mutation_in_swapper(
467        _state: &mut State,
468        meta_state: MetaState,
469        _notifier: Notifier,
470    ) {
471        let mut ms = meta_state.lock().expect("should be able to acquire the MetaState");
472        ms.db_swapper.register_mutation();
473        ms.inactivity_monitor.notify_activity();
474    }
475
476    /// Calls [`lang::db::AnalysisDatabaseSwapper::maybe_swap`] to do its work.
477    fn maybe_swap_database(state: &mut State, meta_state: MetaState, _notifier: Notifier) {
478        meta_state
479            .lock()
480            .expect("should be able to acquire the MetaState")
481            .db_swapper
482            .maybe_swap(&mut state.db, &state.open_files);
483    }
484
485    /// Calls [`lang::diagnostics::DiagnosticsController::refresh`] to do its work.
486    fn refresh_diagnostics(state: &mut State, _meta_state: MetaState, _notifier: Notifier) {
487        state.diagnostics_controller.refresh(
488            &state.db,
489            &state.open_files,
490            &state.config,
491            &state.project_controller.configs_registry(),
492        );
493    }
494
495    /// Reload config and update project model for all open files.
496    fn reload(state: &mut State, requester: &mut Requester<'_>) -> LSPResult<()> {
497        state.project_controller.clear_loaded_workspaces();
498        state.config.reload(requester, &state.client_capabilities)?;
499
500        for uri in state.open_files.iter() {
501            let Some(file_id) = state.db.file_for_url(uri) else { continue };
502            if let FileLongId::OnDisk(file_path) = file_id.long(&state.db) {
503                state.project_controller.request_updating_project_for_file(file_path.clone());
504            }
505        }
506
507        Ok(())
508    }
509}