use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum VarKind {
Scalar,
Array,
Hash,
}
impl VarKind {
#[inline]
pub const fn sigil(self) -> &'static str {
match self {
VarKind::Scalar => "$",
VarKind::Array => "@",
VarKind::Hash => "%",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum SymbolKind {
Package,
Class,
Role,
Subroutine,
Method,
Variable(VarKind),
Constant,
Import,
Export,
Label,
Format,
}
impl SymbolKind {
#[inline]
const fn base_lsp_kind(self) -> u32 {
match self {
SymbolKind::Package => 2, SymbolKind::Class => 5, SymbolKind::Role => 8, SymbolKind::Subroutine => 12, SymbolKind::Method => 6, SymbolKind::Variable(_) => 13, SymbolKind::Constant => 14, SymbolKind::Import => 2, SymbolKind::Export => 12, SymbolKind::Label => 20, SymbolKind::Format => 23, }
}
#[inline]
pub const fn to_lsp_kind(self) -> u32 {
self.base_lsp_kind()
}
#[inline]
pub const fn to_lsp_kind_document_symbol(self) -> u32 {
match self {
SymbolKind::Variable(VarKind::Scalar) => 13, SymbolKind::Variable(VarKind::Array) => 18, SymbolKind::Variable(VarKind::Hash) => 19, _ => self.base_lsp_kind(),
}
}
#[inline]
pub const fn sigil(self) -> Option<&'static str> {
match self {
SymbolKind::Variable(vk) => Some(vk.sigil()),
_ => None,
}
}
#[inline]
pub const fn is_variable(self) -> bool {
matches!(self, SymbolKind::Variable(_))
}
#[inline]
pub const fn is_callable(self) -> bool {
matches!(self, SymbolKind::Subroutine | SymbolKind::Method)
}
#[inline]
pub const fn is_namespace(self) -> bool {
matches!(self, SymbolKind::Package | SymbolKind::Class | SymbolKind::Role)
}
#[inline]
pub const fn scalar() -> Self {
SymbolKind::Variable(VarKind::Scalar)
}
#[inline]
pub const fn array() -> Self {
SymbolKind::Variable(VarKind::Array)
}
#[inline]
pub const fn hash() -> Self {
SymbolKind::Variable(VarKind::Hash)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_var_kind_sigils() {
assert_eq!(VarKind::Scalar.sigil(), "$");
assert_eq!(VarKind::Array.sigil(), "@");
assert_eq!(VarKind::Hash.sigil(), "%");
}
#[test]
fn test_symbol_kind_sigils() {
assert_eq!(SymbolKind::Variable(VarKind::Scalar).sigil(), Some("$"));
assert_eq!(SymbolKind::Variable(VarKind::Array).sigil(), Some("@"));
assert_eq!(SymbolKind::Variable(VarKind::Hash).sigil(), Some("%"));
assert_eq!(SymbolKind::Subroutine.sigil(), None);
assert_eq!(SymbolKind::Package.sigil(), None);
}
#[test]
fn test_lsp_kind_mapping() {
assert_eq!(SymbolKind::Package.to_lsp_kind(), 2);
assert_eq!(SymbolKind::Class.to_lsp_kind(), 5);
assert_eq!(SymbolKind::Method.to_lsp_kind(), 6);
assert_eq!(SymbolKind::Role.to_lsp_kind(), 8);
assert_eq!(SymbolKind::Subroutine.to_lsp_kind(), 12);
assert_eq!(SymbolKind::Variable(VarKind::Scalar).to_lsp_kind(), 13);
assert_eq!(SymbolKind::Constant.to_lsp_kind(), 14);
assert_eq!(SymbolKind::Label.to_lsp_kind(), 20);
assert_eq!(SymbolKind::Format.to_lsp_kind(), 23);
}
#[test]
fn test_lsp_kind_document_symbol_mapping() {
assert_eq!(SymbolKind::Package.to_lsp_kind_document_symbol(), 2);
assert_eq!(SymbolKind::Class.to_lsp_kind_document_symbol(), 5);
assert_eq!(SymbolKind::Subroutine.to_lsp_kind_document_symbol(), 12);
assert_eq!(SymbolKind::Variable(VarKind::Scalar).to_lsp_kind_document_symbol(), 13); assert_eq!(SymbolKind::Variable(VarKind::Array).to_lsp_kind_document_symbol(), 18); assert_eq!(SymbolKind::Variable(VarKind::Hash).to_lsp_kind_document_symbol(), 19); }
#[test]
fn test_non_variable_document_mapping_matches_workspace_mapping() {
let non_variable_kinds = [
SymbolKind::Package,
SymbolKind::Class,
SymbolKind::Role,
SymbolKind::Subroutine,
SymbolKind::Method,
SymbolKind::Constant,
SymbolKind::Import,
SymbolKind::Export,
SymbolKind::Label,
SymbolKind::Format,
];
for kind in non_variable_kinds {
assert_eq!(kind.to_lsp_kind_document_symbol(), kind.to_lsp_kind());
}
}
#[test]
fn test_convenience_constructors() {
assert_eq!(SymbolKind::scalar(), SymbolKind::Variable(VarKind::Scalar));
assert_eq!(SymbolKind::array(), SymbolKind::Variable(VarKind::Array));
assert_eq!(SymbolKind::hash(), SymbolKind::Variable(VarKind::Hash));
}
#[test]
fn test_category_predicates() {
assert!(SymbolKind::Variable(VarKind::Scalar).is_variable());
assert!(!SymbolKind::Subroutine.is_variable());
assert!(SymbolKind::Subroutine.is_callable());
assert!(SymbolKind::Method.is_callable());
assert!(!SymbolKind::Variable(VarKind::Scalar).is_callable());
assert!(SymbolKind::Package.is_namespace());
assert!(SymbolKind::Class.is_namespace());
assert!(SymbolKind::Role.is_namespace());
assert!(!SymbolKind::Subroutine.is_namespace());
}
#[test]
fn test_category_predicates_exhaustive() -> Result<(), Box<dyn std::error::Error>> {
assert!(SymbolKind::Variable(VarKind::Array).is_variable());
assert!(SymbolKind::Variable(VarKind::Hash).is_variable());
assert!(!SymbolKind::Import.is_variable());
assert!(!SymbolKind::Export.is_variable());
assert!(!SymbolKind::Label.is_variable());
assert!(!SymbolKind::Format.is_variable());
assert!(!SymbolKind::Constant.is_variable());
assert!(!SymbolKind::Package.is_callable());
assert!(!SymbolKind::Import.is_callable());
assert!(!SymbolKind::Label.is_callable());
assert!(!SymbolKind::Constant.is_callable());
assert!(!SymbolKind::Subroutine.is_namespace());
assert!(!SymbolKind::Import.is_namespace());
assert!(!SymbolKind::Label.is_namespace());
assert!(!SymbolKind::Format.is_namespace());
Ok(())
}
#[test]
fn test_non_variable_sigil_is_none() -> Result<(), Box<dyn std::error::Error>> {
for kind in [
SymbolKind::Package,
SymbolKind::Import,
SymbolKind::Export,
SymbolKind::Label,
SymbolKind::Format,
SymbolKind::Constant,
] {
assert_eq!(kind.sigil(), None, "{kind:?} should have sigil None");
}
Ok(())
}
#[test]
fn var_kind_is_copy() -> Result<(), Box<dyn std::error::Error>> {
let a = VarKind::Scalar;
let b = a; assert_eq!(a, b);
Ok(())
}
#[test]
fn var_kind_hash_and_eq_consistency() -> Result<(), Box<dyn std::error::Error>> {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(VarKind::Scalar);
set.insert(VarKind::Array);
set.insert(VarKind::Hash);
set.insert(VarKind::Scalar);
assert_eq!(set.len(), 3);
assert!(set.contains(&VarKind::Scalar));
assert!(set.contains(&VarKind::Array));
assert!(set.contains(&VarKind::Hash));
Ok(())
}
#[test]
fn symbol_kind_hash_and_eq_consistency() -> Result<(), Box<dyn std::error::Error>> {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(SymbolKind::Package);
set.insert(SymbolKind::Variable(VarKind::Scalar));
set.insert(SymbolKind::Variable(VarKind::Array));
set.insert(SymbolKind::Package); assert_eq!(set.len(), 3);
assert!(set.contains(&SymbolKind::Variable(VarKind::Scalar)));
assert_ne!(SymbolKind::Variable(VarKind::Scalar), SymbolKind::Variable(VarKind::Array));
Ok(())
}
#[test]
fn var_kind_serde_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
let serialized = serde_json::to_string(&VarKind::Scalar)?;
let deserialized: VarKind = serde_json::from_str(&serialized)?;
assert_eq!(deserialized, VarKind::Scalar);
let serialized = serde_json::to_string(&VarKind::Hash)?;
let deserialized: VarKind = serde_json::from_str(&serialized)?;
assert_eq!(deserialized, VarKind::Hash);
Ok(())
}
#[test]
fn symbol_kind_serde_roundtrip_all_variants() -> Result<(), Box<dyn std::error::Error>> {
let variants = [
SymbolKind::Package,
SymbolKind::Class,
SymbolKind::Role,
SymbolKind::Subroutine,
SymbolKind::Method,
SymbolKind::Variable(VarKind::Scalar),
SymbolKind::Variable(VarKind::Array),
SymbolKind::Variable(VarKind::Hash),
SymbolKind::Constant,
SymbolKind::Import,
SymbolKind::Export,
SymbolKind::Label,
SymbolKind::Format,
];
for kind in variants {
let serialized = serde_json::to_string(&kind)?;
let deserialized: SymbolKind = serde_json::from_str(&serialized)?;
assert_eq!(deserialized, kind, "roundtrip failed for {kind:?}");
}
Ok(())
}
}