mod builtins;
mod resolve;
use std::collections::HashMap;
use std::path::PathBuf;
use anyhow::{Result, anyhow};
use serde::Deserialize;
use super::editor::EditorToml;
#[derive(Debug, Default, Deserialize, Clone)]
pub struct LspToml {
pub command: Option<String>,
pub args: Option<Vec<String>>,
pub language_id: Option<String>,
pub root_markers: Option<Vec<String>>,
}
#[derive(Debug, Clone)]
pub struct LspConfig {
pub name: String,
pub command: String,
pub args: Vec<String>,
pub language_id: Option<String>,
pub root_markers: Vec<String>,
}
impl LspConfig {
fn overlay(&mut self, user: LspToml) {
if let Some(c) = user.command {
self.command = c;
}
if let Some(a) = user.args {
self.args = a;
}
if user.language_id.is_some() {
self.language_id = user.language_id;
}
if let Some(r) = user.root_markers {
self.root_markers = r;
}
}
fn from_user(name: &str, user: LspToml) -> Result<Self> {
let command = user.command.ok_or_else(|| {
anyhow!(
"[lsp.{}] is a new server (no built-in to overlay onto) and \
must define `command`",
name
)
})?;
Ok(Self {
name: name.to_string(),
command,
args: user.args.unwrap_or_default(),
language_id: user.language_id,
root_markers: user.root_markers.unwrap_or_default(),
})
}
}
#[derive(Debug, Default, Deserialize, Clone)]
pub struct FormatterToml {
pub command: Option<String>,
pub args: Option<Vec<String>>,
}
#[derive(Debug, Clone)]
pub struct FormatterConfig {
pub command: String,
pub args: Vec<String>,
}
#[derive(Debug, Default, Deserialize, Clone)]
pub struct LanguageConfig {
pub extensions: Option<Vec<String>>,
pub grammar: Option<String>,
pub grammar_dir: Option<PathBuf>,
pub query_dir: Option<PathBuf>,
pub comment_token: Option<String>,
#[serde(default, flatten)]
pub editor: EditorToml,
pub lsp: Option<Vec<String>>,
pub formatter: Option<FormatterToml>,
}
impl LanguageConfig {
pub fn overlay(&mut self, user: LanguageConfig) {
if user.extensions.is_some() {
self.extensions = user.extensions;
}
if user.grammar.is_some() {
self.grammar = user.grammar;
}
if user.grammar_dir.is_some() {
self.grammar_dir = user.grammar_dir;
}
if user.query_dir.is_some() {
self.query_dir = user.query_dir;
}
if user.comment_token.is_some() {
self.comment_token = user.comment_token;
}
if user.editor.indent_width.is_some() {
self.editor.indent_width = user.editor.indent_width;
}
if user.editor.tab_width.is_some() {
self.editor.tab_width = user.editor.tab_width;
}
if user.editor.use_tabs.is_some() {
self.editor.use_tabs = user.editor.use_tabs;
}
if user.editor.show_whitespace.is_some() {
self.editor.show_whitespace = user.editor.show_whitespace;
}
if user.lsp.is_some() {
self.lsp = user.lsp;
}
if user.formatter.is_some() {
self.formatter = user.formatter;
}
}
}
#[derive(Debug, Clone)]
pub struct Language {
pub name: String,
pub extensions: Vec<String>,
pub grammar: String,
pub grammar_dir: Option<PathBuf>,
pub query_dir: Option<PathBuf>,
pub comment_token: Option<String>,
pub editor: EditorToml,
pub lsp: Vec<LspConfig>,
pub formatter: Option<FormatterConfig>,
}
#[derive(Debug, Clone, Default)]
pub struct LanguageRegistry {
by_name: HashMap<String, Language>,
extension_to_name: HashMap<String, String>,
extension_to_language_id: HashMap<String, String>,
}
impl LanguageRegistry {
pub fn build(
user_languages: HashMap<String, LanguageConfig>,
user_lsp: HashMap<String, LspToml>,
) -> Result<Self> {
let lsp_table = resolve::resolve_lsp_table(user_lsp)?;
let by_name = resolve::resolve(user_languages, &lsp_table)?;
let extension_to_name = resolve::build_extension_index(&by_name);
Ok(Self {
by_name,
extension_to_name,
extension_to_language_id: builtins::builtin_extension_language_ids(),
})
}
pub fn by_extension(&self, ext: &str) -> Option<&Language> {
let name = self.extension_to_name.get(ext)?;
self.by_name.get(name)
}
pub fn language_id_for_extension(&self, ext: &str) -> Option<&str> {
self.extension_to_language_id.get(ext).map(String::as_str)
}
pub fn iter(&self) -> impl Iterator<Item = &Language> {
self.by_name.values()
}
}
#[cfg(test)]
mod tests {
use super::builtins::{builtin_languages, builtin_lsp};
use super::resolve::{build_extension_index, resolve, resolve_lsp_table};
use super::*;
fn empty_lsp() -> HashMap<String, LspConfig> {
resolve_lsp_table(HashMap::new()).unwrap()
}
#[test]
fn builtins_include_rust() {
let m = builtin_languages();
assert!(m.contains_key("rust"));
assert_eq!(
m["rust"].extensions.as_deref(),
Some(&["rs".to_string()][..])
);
}
#[test]
fn builtin_lsp_includes_vtsls_and_tsserver() {
let m = builtin_lsp();
assert!(m.contains_key("vtsls"));
assert!(m.contains_key("typescript-language-server"));
}
#[test]
fn overlay_replaces_only_provided_fields() {
let mut base = LanguageConfig {
extensions: Some(vec!["rs".into()]),
grammar: Some("rust".into()),
..Default::default()
};
let user = LanguageConfig {
grammar: Some("rust-tree-sitter".into()),
..Default::default()
};
base.overlay(user);
assert_eq!(base.grammar.as_deref(), Some("rust-tree-sitter"));
assert_eq!(base.extensions.as_deref(), Some(&["rs".to_string()][..]));
}
#[test]
fn resolve_adds_user_only_language() {
let mut user = HashMap::new();
user.insert(
"fish".into(),
LanguageConfig {
extensions: Some(vec!["fish".into()]),
..Default::default()
},
);
let langs = resolve(user, &empty_lsp()).unwrap();
assert!(langs.contains_key("fish"));
assert_eq!(langs["fish"].grammar, "fish");
assert!(langs["fish"].lsp.is_empty());
}
#[test]
fn resolve_falls_back_to_default_when_user_omits_field() {
let mut user = HashMap::new();
user.insert(
"rust".into(),
LanguageConfig {
grammar: Some("rust-custom".into()),
..Default::default()
},
);
let langs = resolve(user, &empty_lsp()).unwrap();
assert_eq!(langs["rust"].grammar, "rust-custom");
assert_eq!(langs["rust"].extensions, vec!["rs"]);
assert_eq!(langs["rust"].lsp.len(), 1);
assert_eq!(langs["rust"].lsp[0].name, "rust-analyzer");
}
#[test]
fn extension_index_routes_to_language_name() {
let langs = resolve(HashMap::new(), &empty_lsp()).unwrap();
let idx = build_extension_index(&langs);
assert_eq!(idx.get("rs"), Some(&"rust".to_string()));
assert_eq!(idx.get("py"), Some(&"python".to_string()));
}
#[test]
fn typescript_resolves_to_two_servers() {
let langs = resolve(HashMap::new(), &empty_lsp()).unwrap();
let ts = &langs["typescript"];
let names: Vec<&str> = ts.lsp.iter().map(|c| c.name.as_str()).collect();
assert_eq!(names, vec!["vtsls", "typescript-language-server"]);
}
#[test]
fn user_lsp_overlay_replaces_only_provided_fields() {
let mut user_lsp: HashMap<String, LspToml> = HashMap::new();
user_lsp.insert(
"vtsls".into(),
LspToml {
args: Some(vec!["--my-flag".into()]),
..Default::default()
},
);
let table = resolve_lsp_table(user_lsp).unwrap();
let entry = &table["vtsls"];
assert_eq!(entry.command, "vtsls"); assert_eq!(entry.args, vec!["--my-flag"]); }
#[test]
fn user_lsp_new_entry_requires_command() {
let mut user_lsp: HashMap<String, LspToml> = HashMap::new();
user_lsp.insert(
"my-server".into(),
LspToml {
args: Some(vec!["--stdio".into()]),
..Default::default()
},
);
assert!(resolve_lsp_table(user_lsp).is_err());
}
#[test]
fn user_lsp_new_entry_with_command_succeeds() {
let mut user_lsp: HashMap<String, LspToml> = HashMap::new();
user_lsp.insert(
"my-server".into(),
LspToml {
command: Some("my-bin".into()),
args: Some(vec!["--stdio".into()]),
..Default::default()
},
);
let table = resolve_lsp_table(user_lsp).unwrap();
assert_eq!(table["my-server"].command, "my-bin");
}
#[test]
fn language_ref_to_unknown_server_errors() {
let mut user_langs: HashMap<String, LanguageConfig> = HashMap::new();
user_langs.insert(
"rust".into(),
LanguageConfig {
lsp: Some(vec!["nonexistent".into()]),
..Default::default()
},
);
let err = resolve(user_langs, &empty_lsp()).unwrap_err();
assert!(err.to_string().contains("nonexistent"));
}
#[test]
fn user_can_pick_subset_of_servers() {
let mut user_langs: HashMap<String, LanguageConfig> = HashMap::new();
user_langs.insert(
"typescript".into(),
LanguageConfig {
lsp: Some(vec!["typescript-language-server".into()]),
..Default::default()
},
);
let langs = resolve(user_langs, &empty_lsp()).unwrap();
assert_eq!(langs["typescript"].lsp.len(), 1);
assert_eq!(
langs["typescript"].lsp[0].name,
"typescript-language-server"
);
}
}