use crate::detect::DetectionResult;
use crate::mason::registry::{MasonDownload, MasonNeovim, MasonPackage, MasonSource, OneOrMany};
use crate::runtime_state::RuntimeState;
use crate::suggest::SuggestedLanguage;
use std::collections::BTreeMap;
use std::ffi::{OsStr, OsString};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use tempfile::TempDir;
pub(crate) const LOCAL_SHARE_LSP_CLI: &str = ".local/share/lsp-cli";
#[cfg(test)]
pub(crate) const SUBPROCESS_HELPER_MODE_ENV: &str = "LSP_CLI_TEST_HELPER_MODE";
#[cfg(test)]
pub(crate) const SUBPROCESS_HELPER_OUTPUT_PATH_ENV: &str = "LSP_CLI_TEST_HELPER_OUTPUT_PATH";
#[cfg(test)]
pub(crate) const SUBPROCESS_HELPER_STDERR_ENV: &str = "LSP_CLI_TEST_HELPER_STDERR";
#[cfg(test)]
pub(crate) const SUBPROCESS_HELPER_EXIT_CODE_ENV: &str = "LSP_CLI_TEST_HELPER_EXIT_CODE";
#[cfg(test)]
pub(crate) const SUBPROCESS_HELPER_TEST_NAME: &str = "test_support::tests::subprocess_helper";
pub(crate) struct TestDir {
dir: TempDir,
}
impl TestDir {
pub(crate) fn new(prefix: &str) -> Self {
let dir = tempfile::Builder::new()
.prefix(&format!("lsp-cli-{prefix}-test-"))
.tempdir()
.expect("temp dir should be created");
Self { dir }
}
pub(crate) fn path(&self) -> &Path {
self.dir.path()
}
pub(crate) fn write_file(&self, relative: &str, contents: impl AsRef<[u8]>) -> PathBuf {
let path = self.path().join(relative);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("parent dirs should be created");
}
fs::write(&path, contents).expect("file should be written");
path
}
}
struct EnvGuard {
saved: Vec<(String, Option<OsString>)>,
}
impl EnvGuard {
fn new(vars: &[(&str, OsString)]) -> Self {
Self::with_removed(vars, &[])
}
fn with_removed(vars: &[(&str, OsString)], removed: &[&str]) -> Self {
let saved = vars
.iter()
.map(|(name, _)| ((*name).to_string(), std::env::var_os(name)))
.chain(
removed
.iter()
.map(|name| ((*name).to_string(), std::env::var_os(name))),
)
.collect::<Vec<_>>();
for (name, value) in vars {
unsafe { std::env::set_var(name, value) };
}
for name in removed {
unsafe { std::env::remove_var(name) };
}
Self { saved }
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
for (name, value) in &self.saved {
match value {
Some(value) => {
unsafe { std::env::set_var(name, value) };
}
None => {
unsafe { std::env::remove_var(name) };
}
}
}
}
}
fn env_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
pub(crate) fn env_var(name: &'static str, value: impl AsRef<OsStr>) -> (&'static str, OsString) {
(name, value.as_ref().to_os_string())
}
pub(crate) fn with_env_vars<T>(vars: &[(&str, OsString)], run: impl FnOnce() -> T) -> T {
let _lock = env_lock().lock().expect("env lock should be available");
let _guard = EnvGuard::new(vars);
run()
}
pub(crate) fn without_env_vars<T>(vars: &[&str], run: impl FnOnce() -> T) -> T {
let _lock = env_lock().lock().expect("env lock should be available");
let _guard = EnvGuard::with_removed(&[], vars);
run()
}
pub(crate) fn runtime_state_in_home(home: &Path) -> RuntimeState {
RuntimeState::new(home.join(LOCAL_SHARE_LSP_CLI))
}
pub(crate) fn write_registry(state: &RuntimeState, packages: &[MasonPackage]) {
fs::create_dir_all(state.registry_dir()).expect("registry dir should be created");
let bytes = serde_json::to_vec(packages).expect("registry should serialize");
fs::write(state.registry_json_path(), bytes).expect("registry should be written");
}
pub(crate) fn pyright_package() -> MasonPackage {
MasonPackage {
name: "pyright".to_string(),
categories: vec!["LSP".to_string()],
source: MasonSource {
id: "pkg:npm/pyright@1.1.409".to_string(),
extra_packages: Vec::new(),
asset: None,
download: None,
version_overrides: Vec::new(),
},
bin: BTreeMap::from([(
"pyright-langserver".to_string(),
"npm:pyright-langserver".to_string(),
)]),
share: BTreeMap::new(),
neovim: MasonNeovim {
lspconfig: Some("pyright".to_string()),
},
}
}
pub(crate) fn jdtls_package() -> MasonPackage {
MasonPackage {
name: "jdtls".to_string(),
categories: vec!["LSP".to_string()],
source: MasonSource {
id: "pkg:generic/eclipse/eclipse.jdt.ls@v1.0.0".to_string(),
extra_packages: Vec::new(),
asset: None,
download: Some(OneOrMany::Many(vec![MasonDownload {
target: Some(OneOrMany::One("linux".to_string())),
files: BTreeMap::from([(
"jdtls.tar.gz".to_string(),
"https://example.invalid/jdtls.tar.gz".to_string(),
)]),
bin: None,
config: Some("config_linux/".to_string()),
man: None,
}])),
version_overrides: Vec::new(),
},
bin: BTreeMap::from([("jdtls".to_string(), "python:bin/jdtls".to_string())]),
share: BTreeMap::from([
("jdtls/plugins/".to_string(), "plugins/".to_string()),
(
"jdtls/config/".to_string(),
"{{source.download.config}}".to_string(),
),
]),
neovim: MasonNeovim {
lspconfig: Some("jdtls".to_string()),
},
}
}
pub(crate) fn suggested_language(
program: &str,
config_id: &str,
server: &str,
language: &str,
) -> SuggestedLanguage {
SuggestedLanguage {
config_id: config_id.to_string(),
languages: vec![language.to_string()],
server: server.to_string(),
command: vec![program.to_string(), "--stdio".to_string()],
workspace_root: PathBuf::from("."),
wait_for_index: false,
}
}
pub(crate) fn detection_result(filetypes: &[&str], filenames: &[&str]) -> DetectionResult {
DetectionResult {
filetypes: filetypes
.iter()
.map(|filetype| (*filetype).to_string())
.collect(),
filenames: filenames
.iter()
.map(|filename| (*filename).to_string())
.collect(),
}
}
#[cfg(test)]
pub(crate) fn current_test_executable() -> PathBuf {
std::env::current_exe().expect("current test executable should resolve")
}
#[cfg(test)]
pub(crate) fn subprocess_helper_command() -> Vec<String> {
vec![
current_test_executable().display().to_string(),
"--quiet".to_string(),
"--ignored".to_string(),
"--exact".to_string(),
SUBPROCESS_HELPER_TEST_NAME.to_string(),
"--nocapture".to_string(),
]
}
#[cfg(test)]
pub(crate) fn subprocess_helper_env(
mode: &str,
extra: &[(&'static str, OsString)],
) -> Vec<(&'static str, OsString)> {
let mut vars = Vec::with_capacity(extra.len() + 1);
vars.push(env_var(SUBPROCESS_HELPER_MODE_ENV, mode));
vars.extend(extra.iter().cloned());
vars
}
#[cfg(unix)]
pub(crate) fn make_executable(path: &Path) {
use std::os::unix::fs::PermissionsExt;
let mut permissions = fs::metadata(path)
.expect("metadata should be available")
.permissions();
permissions.set_mode(0o755);
fs::set_permissions(path, permissions).expect("permissions should be updated");
}
#[cfg(test)]
mod tests {
use super::{
SUBPROCESS_HELPER_EXIT_CODE_ENV, SUBPROCESS_HELPER_MODE_ENV,
SUBPROCESS_HELPER_OUTPUT_PATH_ENV, SUBPROCESS_HELPER_STDERR_ENV,
};
use std::fs;
use std::io::Write as _;
#[test]
#[ignore = "subprocess helper"]
fn subprocess_helper() {
let Some(mode) = std::env::var_os(SUBPROCESS_HELPER_MODE_ENV) else {
return;
};
match mode.to_string_lossy().as_ref() {
"write-cwd" => {
let output = std::env::var_os(SUBPROCESS_HELPER_OUTPUT_PATH_ENV)
.expect("helper output path should be configured");
let cwd = std::env::current_dir().expect("helper cwd should resolve");
fs::write(output, cwd.display().to_string()).expect("helper cwd should be written");
std::process::exit(0);
}
"stderr-and-exit" => {
let stderr = std::env::var(SUBPROCESS_HELPER_STDERR_ENV).unwrap_or_default();
let code = std::env::var(SUBPROCESS_HELPER_EXIT_CODE_ENV)
.ok()
.and_then(|value| value.parse::<i32>().ok())
.unwrap_or(0);
let mut handle = std::io::stderr().lock();
handle
.write_all(stderr.as_bytes())
.expect("helper stderr should write");
handle.flush().expect("helper stderr should flush");
std::process::exit(code);
}
other => panic!("unknown subprocess helper mode {other:?}"),
}
}
}