use std::path::{Path, PathBuf};
use serde_json::{json, Value};
use crate::ast::Language;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Readiness {
Initialized,
Quiescent,
ProjectLoaded,
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum RootRule {
Outermost(&'static [&'static str]),
NearestOrIndexRoot(&'static [&'static str]),
WorkspaceOrModule {
workspace: &'static [&'static str],
module: &'static [&'static str],
},
}
pub(crate) struct Launch {
pub id: &'static str,
pub programs: &'static [&'static str],
pub program_env: &'static str,
pub args: &'static [&'static str],
pub project_bin: Option<&'static str>,
pub readiness: Readiness,
pub hidden_path_safe_root: bool,
}
pub(crate) struct ServerSpec {
pub id: &'static str,
pub launches: &'static [Launch],
pub language_id: fn(Language) -> &'static str,
pub root: RootRule,
initialization: fn() -> Value,
settings: fn(&str) -> Value,
}
impl ServerSpec {
pub(crate) fn initialization_options(&self) -> Value {
(self.initialization)()
}
pub(crate) fn settings(&self) -> fn(&str) -> Value {
self.settings
}
pub(crate) fn workspace_root(&self, file: &Path, index_root: &Path) -> Option<PathBuf> {
let ancestors = || {
file.ancestors()
.skip(1)
.take_while(|dir| dir.starts_with(index_root))
};
let marked = |dir: &&Path, markers: &[&str]| markers.iter().any(|m| dir.join(m).exists());
match self.root {
RootRule::Outermost(markers) => ancestors()
.filter(|dir| marked(dir, markers))
.last()
.map(Path::to_path_buf),
RootRule::NearestOrIndexRoot(markers) => Some(
ancestors()
.find(|dir| marked(dir, markers))
.unwrap_or(index_root)
.to_path_buf(),
),
RootRule::WorkspaceOrModule { workspace, module } => ancestors()
.filter(|dir| marked(dir, workspace))
.last()
.or_else(|| ancestors().find(|dir| marked(dir, module)))
.map(Path::to_path_buf),
}
}
}
impl Launch {
pub(crate) fn resolve_program(&self, root: &Path) -> Result<String, String> {
#[cfg(test)]
if let Some(program) = test_override::get(self.id) {
return Ok(program);
}
if let Some(program) = std::env::var_os(self.program_env) {
return Ok(program.to_string_lossy().into_owned());
}
let project = self.project_bin.map(|dir| root.join(dir));
self.programs
.iter()
.find_map(|program| {
project
.as_deref()
.and_then(|dir| find_in(dir, program))
.or_else(|| find_on_path(program))
})
.map(|path| path.to_string_lossy().into_owned())
.ok_or_else(|| {
format!(
"{} is not installed (looked for {})",
self.id,
self.programs.join(", ")
)
})
}
}
fn find_in(dir: &Path, program: &str) -> Option<PathBuf> {
[dir.join(program), dir.join(format!("{program}.exe"))]
.into_iter()
.find(|candidate| candidate.is_file())
}
fn find_on_path(program: &str) -> Option<PathBuf> {
let path = std::env::var_os("PATH")?;
std::env::split_paths(&path).find_map(|dir| find_in(&dir, program))
}
const RUST_ANALYZER: ServerSpec = ServerSpec {
id: "rust",
launches: &[Launch {
id: "rust-analyzer",
programs: &["rust-analyzer"],
program_env: "HARN_LSP_SERVER_RUST_ANALYZER",
args: &[],
project_bin: None,
readiness: Readiness::Quiescent,
hidden_path_safe_root: false,
}],
language_id: |_| "rust",
root: RootRule::Outermost(&["Cargo.toml"]),
initialization: rust_analyzer_settings,
settings: |section| match section {
"rust-analyzer" => rust_analyzer_settings(),
_ => Value::Null,
},
};
fn rust_analyzer_settings() -> Value {
json!({
"checkOnSave": false,
"check": {"workspace": false},
"cargo": {"buildScripts": {"enable": false}, "features": "all"},
"procMacro": {"enable": false},
"cachePriming": {"enable": false},
"diagnostics": {"enable": false},
})
}
const TYPESCRIPT: ServerSpec = ServerSpec {
id: "typescript",
launches: &[
Launch {
id: "typescript-language-server",
programs: &["typescript-language-server"],
program_env: "HARN_LSP_SERVER_TYPESCRIPT_LANGUAGE_SERVER",
args: &["--stdio"],
project_bin: Some("node_modules/.bin"),
readiness: Readiness::ProjectLoaded,
hidden_path_safe_root: false,
},
Launch {
id: "tsc-lsp",
programs: &["tsc"],
program_env: "HARN_LSP_SERVER_TSC_LSP",
args: &["--lsp", "--stdio"],
project_bin: Some("node_modules/.bin"),
readiness: Readiness::Initialized,
hidden_path_safe_root: false,
},
],
language_id: |language| match language {
Language::Tsx => "typescriptreact",
Language::JavaScript => "javascript",
Language::Jsx => "javascriptreact",
_ => "typescript",
},
root: RootRule::NearestOrIndexRoot(&["tsconfig.json", "jsconfig.json", "package.json"]),
initialization: || {
json!({"preferences": {
"providePrefixAndSuffixTextForRename": false,
"allowRenameOfImportPath": false,
}})
},
settings: |section| match section {
"typescript" | "javascript" | "js/ts" => {
json!({"preferences": {"useAliasesForRenames": false}})
}
_ => Value::Null,
},
};
const PYRIGHT: ServerSpec = ServerSpec {
id: "python",
launches: &[Launch {
id: "pyright",
programs: &["basedpyright-langserver", "pyright-langserver"],
program_env: "HARN_LSP_SERVER_PYRIGHT",
args: &["--stdio"],
project_bin: None,
readiness: Readiness::Initialized,
hidden_path_safe_root: false,
}],
language_id: |_| "python",
root: RootRule::NearestOrIndexRoot(&[
"pyrightconfig.json",
"pyproject.toml",
"setup.py",
"setup.cfg",
]),
initialization: || Value::Null,
settings: |_| Value::Null,
};
const GOPLS: ServerSpec = ServerSpec {
id: "go",
launches: &[Launch {
id: "gopls",
programs: &["gopls"],
program_env: "HARN_LSP_SERVER_GOPLS",
args: &[],
project_bin: None,
readiness: Readiness::ProjectLoaded,
hidden_path_safe_root: true,
}],
language_id: |_| "go",
root: RootRule::WorkspaceOrModule {
workspace: &["go.work"],
module: &["go.mod"],
},
initialization: || Value::Null,
settings: |_| Value::Null,
};
pub(crate) fn for_language(language: Language) -> Option<&'static ServerSpec> {
match language {
Language::Rust => Some(&RUST_ANALYZER),
Language::TypeScript | Language::Tsx | Language::JavaScript | Language::Jsx => {
Some(&TYPESCRIPT)
}
Language::Python => Some(&PYRIGHT),
Language::Go => Some(&GOPLS),
_ => None,
}
}
#[cfg(test)]
pub(crate) mod test_override {
use std::cell::RefCell;
use std::collections::HashMap;
thread_local! {
static PROGRAMS: RefCell<HashMap<&'static str, String>> = RefCell::new(HashMap::new());
}
pub(crate) fn get(id: &str) -> Option<String> {
PROGRAMS.with(|programs| programs.borrow().get(id).cloned())
}
pub(crate) fn set(id: &'static str, program: &str) {
PROGRAMS.with(|programs| programs.borrow_mut().insert(id, program.to_string()));
}
}