mod client_config;
use std::path::PathBuf;
use lsp_types::InitializeParams;
use crate::cmd_args::CmdArgs;
use crate::context::{ProgressTask, ServerContextSnapshot, get_client_id};
use crate::handlers::register_files_watch;
use crate::logger::init_logger;
use crate::util::uri_to_file_path;
pub use client_config::{ClientConfig, get_client_config};
const IDLE_WAIT: std::time::Duration = std::time::Duration::from_millis(1000);
const INDEX_SLICE: usize = 24;
const COOK_A_SLICE: usize = 4;
const INDEX_SLICE_MS: u64 = 30;
pub async fn initialized_handler(
context: ServerContextSnapshot,
params: InitializeParams,
cmd_args: CmdArgs,
) -> Option<()> {
let roots = workspace_roots(¶ms);
let log_file = init_logger(roots.first().and_then(|root| root.to_str()), &cmd_args);
let client_id = match &cmd_args.editor {
Some(editor) => editor.clone().into(),
None => get_client_id(¶ms.client_info),
};
let supports_config_request = params
.capabilities
.workspace
.as_ref()
.and_then(|workspace| workspace.configuration)
.unwrap_or_default();
log::info!("client {client_id:?}, workspace roots {roots:?}");
let client_config = get_client_config(&context, client_id, supports_config_request).await;
log::info!("client config: {client_config:?}");
{
let mut workspace_manager = context.workspace_manager().lock().await;
workspace_manager.set_roots(roots.clone());
workspace_manager.set_log_file(log_file);
workspace_manager.set_client_config(client_config);
workspace_manager.set_config_file(cmd_args.config.0.clone().map(PathBuf::from));
}
start_analysis(context.clone()).await;
register_files_watch(context.clone()).await;
Some(())
}
pub async fn start_analysis(context: ServerContextSnapshot) {
let Some((root, version)) = open_session(&context).await else {
return;
};
if context.workspace_manager().lock().await.is_closing() {
return;
}
if !context
.lsp_features()
.supports_dynamic_watched_files_registration()
{
let log_file = context
.workspace_manager()
.lock()
.await
.log_file()
.map(PathBuf::from);
if let Some(mut filter) = context
.analysis()
.with_snapshot(|session| session.watch_filter())
{
if let Some(log_file) = log_file {
match std::fs::canonicalize(&log_file) {
Ok(path) => filter = filter.ignore(path),
Err(error) => log::warn!(
"cannot exclude the server log {} from the watcher: {error}",
log_file.display()
),
}
}
if let Err(error) = context
.native_watcher()
.start(context.clone(), root.clone(), filter)
.await
{
context.native_watcher().stop().await;
let message = format!(
"Cannot watch workspace files: {error}. External changes may not be detected."
);
log::error!("{message}");
context.client().show_message(lsp_types::ShowMessageParams {
typ: lsp_types::MessageType::WARNING,
message,
});
}
}
}
if context.workspace_manager().lock().await.is_closing() {
context.native_watcher().stop().await;
return;
}
context.status_bar().update_progress_task(
ProgressTask::LoadWorkspace,
None,
Some(format!("Indexing {}", root.display())),
);
index_in_background(context.clone(), version).await;
context
.diagnostics()
.schedule_open_files(context.clone())
.await;
}
async fn open_session(context: &ServerContextSnapshot) -> Option<(PathBuf, i64)> {
let (root, filter, config_file, version, generation) = {
let workspace_manager = context.workspace_manager().lock().await;
if workspace_manager.is_closing() {
return None;
}
(
workspace_manager.root()?.to_path_buf(),
workspace_manager.watch_filter()?,
workspace_manager.config_file().map(PathBuf::from),
workspace_manager.workspace_version(),
workspace_manager.reload_generation(),
)
};
context
.status_bar()
.create_progress_task(ProgressTask::LoadWorkspace)
.await;
let session = context
.analysis()
.build_session(root.clone(), filter, config_file)
.await?;
if !install_prepared_session(context, session, version, generation).await {
log::debug!("discarding a workspace session superseded while it was being built");
return None;
}
let diagnostic_delay = context
.analysis()
.with_snapshot(|session| session.project_config().config.diagnostics.on_change_ms)
.flatten();
context.diagnostics().set_change_delay(diagnostic_delay);
report_project_configuration(context);
log::info!("analysing {}", root.display());
Some((root, version))
}
async fn install_prepared_session(
context: &ServerContextSnapshot,
mut session: cpp_code_analysis::Session<cpp_code_analysis::DiskFiles>,
version: i64,
generation: u64,
) -> bool {
let workspace_manager = context.workspace_manager().lock().await;
if workspace_manager.is_closing()
|| workspace_manager.workspace_version() != version
|| workspace_manager.reload_generation() != generation
{
return false;
}
let open_files = workspace_manager.workspace_open_files();
for (uri, text) in &open_files {
if let Some(path) = uri_to_file_path(uri) {
session.did_open(path, text);
}
}
context.analysis().install_session(session).await;
if !open_files.is_empty() {
log::info!(
"re-marked {} open buffers as the text the analysis reads",
open_files.len()
);
}
true
}
fn report_project_configuration(context: &ServerContextSnapshot) {
let report = context
.analysis()
.with_snapshot(|session| session.project_config().clone());
let discovery = context
.analysis()
.with_snapshot(|session| session.discovery().clone());
let Some(report) = report else {
return;
};
if report.problems.is_empty() {
if let Some(path) = &report.path {
log::info!(
"project configuration: {} (nothing to report)",
path.display()
);
}
} else {
let path = report
.path
.as_ref()
.map(|path| path.display().to_string())
.unwrap_or_else(|| "(no configuration file)".to_string());
log::warn!(
"project configuration {} has {} problem(s):",
path,
report.problems.len()
);
for problem in &report.problems {
log::warn!(" [{:?}] {}", problem.severity, problem.message);
}
}
if let Some(discovery) = &discovery {
for problem in &discovery.problems {
log::warn!("[{:?}] {}", problem.severity, problem.message);
}
if let Some(database) = &discovery.database {
log::info!(
"compile database: {} ({}, {} entries)",
database.path.display(),
database.origin.words(),
database.commands.len()
);
}
}
let errors: Vec<String> = report
.errors()
.map(|problem| problem.message.clone())
.collect();
if !errors.is_empty() {
let path = report
.path
.as_ref()
.map(|path| path.display().to_string())
.unwrap_or_else(|| "the project configuration".to_string());
context.client().show_message(lsp_types::ShowMessageParams {
typ: lsp_types::MessageType::WARNING,
message: format!("{path} could not be read in full:\n{}", errors.join("\n")),
});
}
}
async fn index_in_background(context: ServerContextSnapshot, version: i64) {
tokio::spawn(async move {
let mut first_pass = true;
let mut last_diagnostic_revision = None;
let mut was_pending = false;
let mut no_progress = 0usize;
const THE_FIRST_WAVE: usize = 8;
let mut wave = THE_FIRST_WAVE;
const PROGRESS_REPORT_EVERY: std::time::Duration = std::time::Duration::from_millis(250);
let mut last_report = std::time::Instant::now();
loop {
log::trace!("pump: round");
if context.workspace_manager().lock().await.workspace_version() != version {
log::debug!("the workspace was reloaded; this indexing pass stops");
return;
}
let open_file = context
.analysis()
.with_snapshot(|session| session.next_open_file_to_read())
.flatten();
if let Some(path) = open_file
&& context.analysis().read_queued_file(&path).await
{
was_pending = true;
continue;
}
let before = context
.analysis()
.with_snapshot(|session| (session.pending_work(), session.revision()));
let prepared = context
.analysis()
.run_blocking(move |session| Some(session.prepare_background_wave(wave)))
.await
.unwrap_or_default();
let prepared_count = prepared.prepared.len();
log::trace!("pump: wave prepared ({prepared_count})");
let Some((pending, drain, claimed, held, committed)) = context
.analysis()
.update_session("index slice", |session| {
let started = std::time::Instant::now();
let (done, drain) = session.commit_a_wave(prepared, wave);
let taken = done.len();
let held = started.elapsed().as_millis();
let claimed = session.claim_cooking(COOK_A_SLICE);
let pending_now = session.pending_work();
let sizes = session.cache_sizes();
if pending_now == 0 && taken > 0 {
log::info!(
"index drained: cache {} rendering(s) ({} KB), {} translation unit(s), {} summary(ies), \\
{} fact(s), {} macro(s), {} cooked file(s) / {} cooked declaration(s)",
sizes.renderings,
sizes.rendering_bytes / 1024,
sizes.units,
sizes.summaries,
sizes.facts,
sizes.macros,
sizes.cooked_files,
sizes.cooked_declarations
);
let report = format!(
"renderings {} / {} KB | units {} | summaries {} / facts {} / macros {} | cooked {} / {}\n{}",
sizes.renderings,
sizes.rendering_bytes / 1024,
sizes.units,
sizes.summaries,
sizes.facts,
sizes.macros,
sizes.cooked_files,
sizes.cooked_declarations,
session.memory_report()
);
let _ = std::fs::write(
std::env::temp_dir().join("cpp_ls-memory.txt"),
report,
);
}
if taken > 0 || prepared_count > 0 || !drain.is_empty() || !claimed.is_empty() {
log::info!(
"index slice: prepared {prepared_count}, committed {taken}, write lock held {held} ms, \
{cooking} to cook, {pending_now} left",
cooking = claimed.len()
);
}
(pending_now, drain, claimed, held, taken)
})
.await
else {
log::warn!("the workspace was closed while it was being indexed");
return;
};
wave = match held {
over if over > u128::from(INDEX_SLICE_MS) => (wave / 2).max(1),
under if under < u128::from(INDEX_SLICE_MS) => (wave * 2).min(INDEX_SLICE),
_ => wave,
};
let drain_was_empty = drain.is_empty();
let asked: Vec<std::path::PathBuf> = claimed.clone();
let claimed_count = asked.len();
log::trace!(
"pump: drain starts (empty {}, {} claimed to cook)",
drain_was_empty,
asked.len()
);
let (prepared_drain, cooks, cooking_revision) = context
.analysis()
.run_blocking(move |session| {
let revision = session.revision();
let cooks = claimed
.iter()
.filter_map(|path| session.gather_a_cook(path))
.collect();
Some((session.prepare_the_drain(drain), cooks, Some(revision)))
})
.await
.unwrap_or_default();
let rendered_cooks = context.analysis().render_cooks(cooks).await;
let built: Vec<std::path::PathBuf> = rendered_cooks
.iter()
.map(|rendered| rendered.indexed.summary.path.clone())
.collect();
log::trace!("pump: commit starts");
let (re_read, cooked) = context
.analysis()
.update_session("index slice", |session| {
if let Some(revision) = &cooking_revision {
if session.is_current(revision) {
session.refuse_the_cooks_that_built_nothing(&asked, &built);
} else if session.owns_revision(revision) {
for path in &asked {
session.want_cooked_reading(path);
}
}
}
let cook_logged = !asked.is_empty();
if !asked.is_empty() {
log::info!(
"cook slice: asked {}, rendered {}, emptied the queue, refused {}",
asked.len(),
built.len(),
asked.len().saturating_sub(built.len())
);
}
let started = std::time::Instant::now();
let re_read = session.commit_the_drain(prepared_drain);
let drained = started.elapsed().as_millis();
let started = std::time::Instant::now();
let cooked = session.commit_them(rendered_cooks).len();
let inserted = started.elapsed().as_millis();
if cook_logged {
log::info!(
"cook slice committed: {cooked} of {} rendering(s) stored, {re_read} re-read",
built.len()
);
}
if re_read > 0 || cooked > 0 {
log::debug!(
"index slice tail: re-read {re_read}, cooked {cooked}, write lock held {held} ms \
(re-read {drained} ms, cooked {inserted} ms)",
held = drained + inserted
);
}
(re_read, cooked)
})
.await
.unwrap_or((0, 0));
if (re_read > 0 || cooked > 0) && pending == 0 {
let warmed = context
.analysis()
.run_blocking(|session| Some(session.warm_the_pulls()))
.await
.unwrap_or(0);
if warmed > 0 {
log::trace!("pump: {warmed} pull answer(s) warmed");
}
}
let after = context.analysis().with_snapshot(|session| {
(
session.pending_work(),
before
.as_ref()
.is_some_and(|(_, revision)| !session.is_current(revision)),
)
});
let pending = after.as_ref().map_or(pending, |(pending, _)| *pending);
let had_work = before.as_ref().is_some_and(|(pending, _)| *pending > 0)
|| claimed_count > 0
|| !drain_was_empty;
let progressed = committed > 0
|| re_read > 0
|| cooked > 0
|| after.as_ref().is_some_and(|(_, changed)| *changed)
|| before
.as_ref()
.is_some_and(|(previous, _)| pending < *previous);
no_progress = if progressed || !had_work {
0
} else {
no_progress + 1
};
if no_progress >= 8 {
let dropped = context
.analysis()
.update_session("drop the files that cannot be built", |session| {
session.abandon_the_cooking_queue()
})
.await
.unwrap_or(0);
log::warn!(
"the pump made no progress in {no_progress} slices with {pending} file(s) left: \
{dropped} queued file(s) were dropped (they are asked for again by the query that names one)"
);
no_progress = 0;
if dropped == 0 {
context.analysis().wait_for_work(IDLE_WAIT).await;
}
}
if pending > 0 {
let to_read = match context
.analysis()
.query_blocking(|session| Some(session.pending()))
.await
{
crate::context::RequestOutcome::Ready(count) => count,
_ => pending,
};
let still = pending.saturating_sub(to_read);
let message = if to_read > 0 {
format!("{to_read} files to read, {still} to re-read")
} else {
format!("re-reading {still} file(s) for the compiler's view")
};
let now = std::time::Instant::now();
if now.duration_since(last_report) >= PROGRESS_REPORT_EVERY {
context.status_bar().update_progress_task(
ProgressTask::LoadWorkspace,
None,
Some(message),
);
last_report = now;
}
was_pending = true;
tokio::task::yield_now().await;
continue;
}
if first_pass || was_pending {
let queued = context
.analysis()
.update_session("queue the modules the open files import", |session| {
session.queue_the_modules_the_open_files_import()
})
.await
.unwrap_or(0);
if queued > 0 {
log::info!("{queued} module interface unit(s) queued for reading");
was_pending = true;
tokio::task::yield_now().await;
continue;
}
}
if first_pass {
first_pass = false;
context.status_bar().finish_progress_task(
ProgressTask::LoadWorkspace,
Some("the workspace is indexed".to_string()),
);
log::info!(
"the workspace is indexed — the longest an update held the analysis was {} ms",
context.analysis().worst_write_hold()
);
context
.analysis()
.update_session("save the sharing cache", |session| {
match session.save_the_sharing() {
Ok(written) => {
log::info!(
"wrote {written} shared contribution(s) for the next run"
)
}
Err(error) => log::warn!("the sharing cache was not written: {error}"),
}
})
.await;
} else if was_pending {
if context.lsp_features().supports_refresh_inlay_hint() {
context.client().refresh_inlay_hints();
}
if context.lsp_features().supports_refresh_semantic_tokens() {
context.client().refresh_semantic_tokens();
}
}
let revision = context
.analysis()
.with_snapshot(|session| {
if last_diagnostic_revision
.as_ref()
.is_some_and(|previous| session.is_current(previous))
{
None
} else {
Some(session.revision())
}
})
.flatten();
if let Some(revision) = revision {
context.diagnostics().refresh_pull(context.clone());
context
.diagnostics()
.schedule_open_files(context.clone())
.await;
last_diagnostic_revision = Some(revision);
}
was_pending = false;
if pending == 0
&& let Some(wait) = context
.analysis()
.with_snapshot(
cpp_code_analysis::Session::the_wait_before_a_program_may_be_read,
)
.flatten()
{
if wait.is_zero() {
if context.analysis().read_a_program().await {
context.diagnostics().refresh_pull(context.clone());
context
.diagnostics()
.schedule_open_files(context.clone())
.await;
if context.lsp_features().supports_refresh_inlay_hint() {
context.client().refresh_inlay_hints();
}
if context.lsp_features().supports_refresh_semantic_tokens() {
context.client().refresh_semantic_tokens();
}
context.analysis().wait_for_work(IDLE_WAIT).await;
}
} else {
context.analysis().wait_for_work(wait).await;
}
continue;
}
if !context.analysis().wait_for_work(IDLE_WAIT).await {
continue;
}
}
});
}
pub fn workspace_roots(params: &InitializeParams) -> Vec<PathBuf> {
let mut roots = Vec::new();
if let Some(folders) = ¶ms.workspace_folders {
for folder in folders {
if let Some(path) = uri_to_file_path(&folder.uri) {
roots.push(path);
}
}
}
if roots.is_empty() {
#[allow(deprecated)]
if let Some(path) = params.root_uri.as_ref().and_then(uri_to_file_path) {
roots.push(path);
}
}
roots
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use cpp_code_analysis::{CompilerConfig, Session, WatchFilter};
use lsp_server::Connection;
use lsp_types::{ClientCapabilities, TextDocumentContentChangeEvent};
use super::*;
use crate::context::{ClientId, ServerContext};
use crate::util::path_to_uri;
fn prepared(
context: &ServerContextSnapshot,
root: &std::path::Path,
) -> Session<cpp_code_analysis::DiskFiles> {
Session::with_config(
root,
context.analysis().files().clone(),
WatchFilter::new(root),
CompilerConfig::default(),
)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_built_session_receives_the_latest_buffer_at_installation() {
let (connection, _client) = Connection::memory();
let server = ServerContext::new(
Arc::new(connection),
ClientCapabilities::default(),
ClientId::Other,
);
let context = server.snapshot();
let root = std::env::temp_dir().join("cpp-ls-install-session");
let path = root.join("main.cpp");
let uri = path_to_uri(&path).unwrap();
let session = prepared(&context, &root);
let (version, generation) = {
let mut manager = context.workspace_manager().lock().await;
manager.set_roots(vec![root]);
manager.open_file(uri.clone(), "int old_name;".into(), 1);
manager
.change_file(
&uri,
2,
&[TextDocumentContentChangeEvent {
range: None,
range_length: None,
text: "int current_name;".into(),
}],
)
.unwrap();
(manager.workspace_version(), manager.reload_generation())
};
assert!(install_prepared_session(&context, session, version, generation).await);
let text = context
.analysis()
.with_snapshot(|session| session.files().held(&path).unwrap().text.clone())
.unwrap();
assert_eq!(text.as_ref(), "int current_name;");
server.close().await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_session_built_for_an_old_workspace_is_not_installed() {
let (connection, _client) = Connection::memory();
let server = ServerContext::new(
Arc::new(connection),
ClientCapabilities::default(),
ClientId::Other,
);
let context = server.snapshot();
let root = std::env::temp_dir().join("cpp-ls-stale-session");
let session = prepared(&context, &root);
let (version, generation) = {
let mut manager = context.workspace_manager().lock().await;
manager.set_roots(vec![root]);
let captured = (manager.workspace_version(), manager.reload_generation());
manager.clear_workspace().await;
captured
};
assert!(!install_prepared_session(&context, session, version, generation).await);
assert!(context.analysis().with_snapshot(|_| ()).is_none());
server.close().await;
}
}