#![warn(missing_docs)]
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CommentConfig {
pub line: Option<String>,
pub block_start: Option<String>,
pub block_end: Option<String>,
}
impl CommentConfig {
pub fn line(token: impl Into<String>) -> Self {
Self {
line: Some(token.into()),
block_start: None,
block_end: None,
}
}
pub fn block(start: impl Into<String>, end: impl Into<String>) -> Self {
Self {
line: None,
block_start: Some(start.into()),
block_end: Some(end.into()),
}
}
pub fn line_and_block(
line: impl Into<String>,
block_start: impl Into<String>,
block_end: impl Into<String>,
) -> Self {
Self {
line: Some(line.into()),
block_start: Some(block_start.into()),
block_end: Some(block_end.into()),
}
}
pub fn has_line(&self) -> bool {
self.line.as_deref().is_some_and(|s| !s.is_empty())
}
pub fn has_block(&self) -> bool {
self.block_start.as_deref().is_some_and(|s| !s.is_empty())
&& self.block_end.as_deref().is_some_and(|s| !s.is_empty())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AutoPair {
pub open: char,
pub close: char,
}
impl AutoPair {
pub const fn new(open: char, close: char) -> Self {
Self { open, close }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AutoPairsConfig {
pub enabled: bool,
pub pairs: Vec<AutoPair>,
pub wrap_selection: bool,
pub skip_over_closing: bool,
pub delete_pair: bool,
}
impl Default for AutoPairsConfig {
fn default() -> Self {
Self {
enabled: false,
pairs: vec![
AutoPair::new('(', ')'),
AutoPair::new('[', ']'),
AutoPair::new('{', '}'),
AutoPair::new('"', '"'),
AutoPair::new('\'', '\''),
AutoPair::new('`', '`'),
],
wrap_selection: true,
skip_over_closing: true,
delete_pair: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct LanguageId(String);
impl LanguageId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<&str> for LanguageId {
fn from(value: &str) -> Self {
Self::new(value)
}
}
impl From<String> for LanguageId {
fn from(value: String) -> Self {
Self::new(value)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IndentStyle {
Tabs,
Spaces(u8),
}
impl Default for IndentStyle {
fn default() -> Self {
Self::Spaces(4)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndentationConfig {
pub style: IndentStyle,
pub indent_triggers: Vec<char>,
pub outdent_triggers: Vec<char>,
}
impl Default for IndentationConfig {
fn default() -> Self {
Self {
style: IndentStyle::default(),
indent_triggers: vec!['{', '[', '(', ':'],
outdent_triggers: vec!['}', ']', ')'],
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct WordBoundaryLanguageConfig {
pub ascii_boundary_chars: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TreeSitterLanguageConfig {
pub grammar: String,
pub query_pack_id: String,
pub enabled_by_default: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LspLanguageConfig {
pub language_id: String,
pub command: String,
pub args: Vec<String>,
pub root_markers: Vec<String>,
}
impl LspLanguageConfig {
pub fn detect_root_dir(&self, file_path: &Path) -> Option<PathBuf> {
let start_dir = if file_path.is_dir() {
file_path
} else {
file_path.parent()?
};
self.detect_root_dir_from_dir(start_dir)
}
pub fn detect_root_dir_from_dir(&self, start_dir: &Path) -> Option<PathBuf> {
let mut dir = start_dir.to_path_buf();
loop {
for marker in &self.root_markers {
if dir.join(marker).exists() {
return Some(dir);
}
}
if !dir.pop() {
break;
}
}
None
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LanguageConfig {
pub id: LanguageId,
pub display_name: String,
pub file_extensions: Vec<String>,
pub file_names: Vec<String>,
pub comments: CommentConfig,
pub auto_pairs: AutoPairsConfig,
pub indentation: IndentationConfig,
pub word_boundary: WordBoundaryLanguageConfig,
pub treesitter: Option<TreeSitterLanguageConfig>,
pub lsp: Option<LspLanguageConfig>,
pub extra: BTreeMap<String, String>,
}
impl LanguageConfig {
pub fn new(id: impl Into<LanguageId>, display_name: impl Into<String>) -> Self {
Self {
id: id.into(),
display_name: display_name.into(),
file_extensions: Vec::new(),
file_names: Vec::new(),
comments: CommentConfig::default(),
auto_pairs: AutoPairsConfig::default(),
indentation: IndentationConfig::default(),
word_boundary: WordBoundaryLanguageConfig::default(),
treesitter: None,
lsp: None,
extra: BTreeMap::new(),
}
}
pub fn with_extension(mut self, ext: impl Into<String>) -> Self {
self.file_extensions.push(ext.into());
self
}
pub fn with_file_name(mut self, name: impl Into<String>) -> Self {
self.file_names.push(name.into());
self
}
pub fn matches_path(&self, path: &Path) -> bool {
if let Some(name) = path.file_name().and_then(|n| n.to_str())
&& self.file_names.iter().any(|x| x == name)
{
return true;
}
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
let ext = ext.to_ascii_lowercase();
if self
.file_extensions
.iter()
.any(|x| x.to_ascii_lowercase() == ext)
{
return true;
}
}
false
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LanguageRegistryError {
DuplicateLanguageId(String),
}
impl std::fmt::Display for LanguageRegistryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::DuplicateLanguageId(id) => write!(f, "duplicate language id: {id}"),
}
}
}
impl std::error::Error for LanguageRegistryError {}
#[derive(Debug, Clone)]
pub struct LanguageRegistry {
languages: Vec<LanguageConfig>,
}
impl LanguageRegistry {
pub fn new() -> Self {
Self {
languages: Vec::new(),
}
}
pub fn register(&mut self, lang: LanguageConfig) -> Result<(), LanguageRegistryError> {
if self
.languages
.iter()
.any(|l| l.id.as_str() == lang.id.as_str())
{
return Err(LanguageRegistryError::DuplicateLanguageId(
lang.id.as_str().to_string(),
));
}
self.languages.push(lang);
Ok(())
}
pub fn languages(&self) -> &[LanguageConfig] {
&self.languages
}
pub fn by_id(&self, id: &str) -> Option<&LanguageConfig> {
self.languages.iter().find(|l| l.id.as_str() == id)
}
pub fn language_for_path(&self, path: &Path) -> Option<&LanguageConfig> {
self.languages.iter().find(|l| l.matches_path(path))
}
}
impl Default for LanguageConfig {
fn default() -> Self {
Self::new("plain-text", "Plain Text")
}
}
impl Default for LanguageRegistry {
fn default() -> Self {
let mut reg = Self::new();
let mut rust = LanguageConfig::new("rust", "Rust").with_extension("rs");
rust.comments = CommentConfig::line_and_block("//", "/*", "*/");
rust.lsp = Some(LspLanguageConfig {
language_id: "rust".to_string(),
command: "rust-analyzer".to_string(),
args: Vec::new(),
root_markers: vec!["Cargo.toml".to_string(), ".git".to_string()],
});
rust.treesitter = Some(TreeSitterLanguageConfig {
grammar: "rust".to_string(),
query_pack_id: "rust".to_string(),
enabled_by_default: true,
});
rust.auto_pairs.enabled = true;
let _ = reg.register(rust);
let mut toml = LanguageConfig::new("toml", "TOML").with_extension("toml");
toml.file_names.push("Cargo.toml".to_string());
toml.comments = CommentConfig::line("#");
toml.auto_pairs.enabled = true;
let _ = reg.register(toml);
let mut md = LanguageConfig::new("markdown", "Markdown")
.with_extension("md")
.with_extension("markdown");
md.comments = CommentConfig::block("<!--", "-->");
md.auto_pairs.enabled = true;
let _ = reg.register(md);
let mut json = LanguageConfig::new("json", "JSON").with_extension("json");
json.comments = CommentConfig::default();
json.auto_pairs.enabled = true;
let _ = reg.register(json);
reg
}
}