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);
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_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) = open_session(&context).await else {
return;
};
context.status_bar().update_progress_task(
ProgressTask::LoadWorkspace,
None,
Some(format!("Indexing {}", root.display())),
);
index_in_background(context).await;
}
async fn open_session(context: &ServerContextSnapshot) -> Option<PathBuf> {
let (root, filter, config_file) = {
let workspace_manager = context.workspace_manager().lock().await;
(
workspace_manager.root()?.to_path_buf(),
workspace_manager.watch_filter()?,
workspace_manager.config_file().map(PathBuf::from),
)
};
context
.status_bar()
.create_progress_task(ProgressTask::LoadWorkspace)
.await;
context
.analysis()
.open(root.clone(), filter, config_file)
.await;
report_project_configuration(context);
let open_files = {
let workspace_manager = context.workspace_manager().lock().await;
workspace_manager.workspace_open_files()
};
context
.analysis()
.update_session("initialized: the sharing cache", |session| {
let restored = session.load_the_sharing();
if restored > 0 {
log::info!("restored {restored} shared contribution(s) from the last run");
}
})
.await;
if !open_files.is_empty() {
let count = open_files.len();
context
.analysis()
.update_session(
"initialized: the files the client had open",
move |session| {
for (uri, text) in &open_files {
if let Some(path) = uri_to_file_path(uri) {
session.did_open(path, text);
}
}
},
)
.await;
log::info!("re-marked {count} open buffers as the text the analysis reads");
}
log::info!("analysing {}", root.display());
Some(root)
}
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) {
tokio::spawn(async move {
let version = context.workspace_manager().lock().await.workspace_version();
let mut first_pass = true;
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 prepared = context
.analysis()
.run_blocking(move |session| Some(session.prepare_a_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)) = 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);
for _ in 0..4 {
if !session.read_a_module_unit() {
break;
}
}
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)
})
.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, rendered_cooks, built) = context
.analysis()
.run_blocking(move |session| {
let materials = session.materials_for(claimed);
let rendered = session.render_them(materials);
let built: Vec<std::path::PathBuf> = rendered
.iter()
.map(|rendered| rendered.indexed.summary.path.clone())
.collect();
Some((session.prepare_the_drain(drain), rendered, built))
})
.await
.unwrap_or_default();
log::trace!("pump: commit starts");
let (re_read, cooked) = context
.analysis()
.update_session("index slice", |session| {
session.refuse_the_cooks_that_built_nothing(&asked, &built);
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 had_work = pending > 0 || claimed_count > 0 || !drain_was_empty;
let progressed = prepared_count > 0
|| !drain_was_empty
|| claimed_count > 0
|| re_read > 0
|| cooked > 0;
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 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();
}
}
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 {
tokio::time::sleep(IDLE_WAIT).await;
}
} else {
tokio::time::sleep(wait + IDLE_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
}