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