From 5820b7112df8e86f78ce7cb0ca11bad68b143990 Mon Sep 17 00:00:00 2001 From: Lukas Scheller Date: Sat, 16 Aug 2025 14:55:15 +0200 Subject: [PATCH] Add support for customizable case transformation in completions --- vhdl_lang/src/config.rs | 130 ++++++++++++++++++++++++++ vhdl_lang/src/lib.rs | 2 +- vhdl_ls/src/vhdl_server.rs | 5 +- vhdl_ls/src/vhdl_server/completion.rs | 93 +++++++++--------- vhdl_ls/src/vhdl_server/lifecycle.rs | 1 + vhdl_ls/src/vhdl_server/workspace.rs | 1 + 6 files changed, 184 insertions(+), 48 deletions(-) diff --git a/vhdl_lang/src/config.rs b/vhdl_lang/src/config.rs index 28332e71c..1c13818b8 100644 --- a/vhdl_lang/src/config.rs +++ b/vhdl_lang/src/config.rs @@ -12,6 +12,7 @@ use std::fs::File; use std::io; use std::io::prelude::*; use std::path::Path; +use std::str::FromStr; use fnv::FnvHashMap; use subst::VariableMap; @@ -21,6 +22,116 @@ use crate::data::error_codes::ErrorCode; use crate::data::*; use crate::standard::VHDLStandard; +/// Defines standard VHDL case conventions. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Case { + /// All lower case, i.e., `std_logic_vector` + Lower, + /// All upper-case, i.e., `STD_LOGIC_VECTOR` + Upper, + /// Pascal case, i.e., `Std_Logic_Vector` + Pascal, +} + +impl FromStr for Case { + type Err = String; + + fn from_str(s: &str) -> Result { + Ok(match s { + "lower" | "snake" => Case::Lower, + "upper" | "upper_snake" => Case::Upper, + "pascal" | "upper_camel" => Case::Pascal, + other => return Err(other.to_string()), + }) + } +} + +impl Case { + /// Converts the case in place, modifying the passed string. + pub fn convert(&self, val: &mut str) { + match self { + Case::Lower => val.make_ascii_lowercase(), + Case::Upper => val.make_ascii_uppercase(), + Case::Pascal => { + // SAFETY: changing ASCII letters only does not invalidate UTF-8. + let bytes = unsafe { val.as_bytes_mut() }; + // First letter should be uppercased + let mut next_uppercase = true; + for byte in bytes { + if byte == &b'_' { + next_uppercase = true; + continue; + } + if next_uppercase { + byte.make_ascii_uppercase(); + } else { + byte.make_ascii_lowercase(); + } + next_uppercase = false; + } + } + } + } +} + +#[cfg(test)] +mod case_tests { + use super::*; + + #[test] + fn test_case_lower() { + let mut test = String::from("STD_LOGIC_VECTOR"); + Case::Lower.convert(&mut test); + assert_eq!(test, "std_logic_vector"); + } + + #[test] + fn test_case_upper() { + let mut test = String::from("std_logic_vector"); + Case::Upper.convert(&mut test); + assert_eq!(test, "STD_LOGIC_VECTOR"); + } + + #[test] + fn test_case_pascal() { + let mut test = String::from("std_logic_vector"); + Case::Pascal.convert(&mut test); + assert_eq!(test, "Std_Logic_Vector"); + } + + #[test] + fn test_case_empty() { + for case in &[Case::Lower, Case::Upper, Case::Pascal] { + let mut test = String::new(); + case.convert(&mut test); + assert_eq!(test, ""); + } + } + + #[test] + fn test_case_underscore_only() { + for case in &[Case::Lower, Case::Upper, Case::Pascal] { + let mut test = String::from("___"); + case.convert(&mut test); + assert_eq!(test, "___"); + } + } + + #[test] + fn test_case_consecutive_underscore() { + let mut test = String::from("std__logic___vector"); + Case::Pascal.convert(&mut test); + assert_eq!(test, "Std__Logic___Vector"); + } + + #[test] + fn test_case_mixed() { + let mut test = String::from("StD_LoGiC_VeCToR"); + Case::Pascal.convert(&mut test); + assert_eq!(test, "Std_Logic_Vector"); + } +} + #[derive(Clone, PartialEq, Eq, Default, Debug)] pub struct Config { // A map from library name to file name @@ -28,6 +139,7 @@ pub struct Config { standard: VHDLStandard, // Defines the severity that diagnostics are displayed with severities: SeverityMap, + preferred_case: Option, } #[derive(Clone, PartialEq, Eq, Default, Debug)] @@ -126,10 +238,22 @@ impl Config { SeverityMap::default() }; + let case = if let Some(case) = config.get("preferred_case") { + Some( + case.as_str() + .ok_or("preferred_case must be a string")? + .parse() + .map_err(|other| format!("Case '{other}' not valid"))?, + ) + } else { + None + }; + Ok(Config { libraries, severities, standard, + preferred_case: case, }) } @@ -192,6 +316,7 @@ impl Config { } } self.severities = config.severities; + self.preferred_case = config.preferred_case; } /// Load configuration file from installation folder @@ -301,6 +426,11 @@ impl Config { pub fn standard(&self) -> VHDLStandard { self.standard } + + /// Returns the casing that is preferred by the user for linting or completions. + pub fn preferred_case(&self) -> Option { + self.preferred_case + } } fn match_file_patterns( diff --git a/vhdl_lang/src/lib.rs b/vhdl_lang/src/lib.rs index 80f348912..149b6504b 100644 --- a/vhdl_lang/src/lib.rs +++ b/vhdl_lang/src/lib.rs @@ -27,7 +27,7 @@ mod completion; mod formatting; mod standard; -pub use crate::config::Config; +pub use crate::config::{Case, Config}; pub use crate::data::{ Diagnostic, Latin1String, Message, MessageHandler, MessagePrinter, MessageType, NullDiagnostics, NullMessages, Position, Range, Severity, SeverityMap, Source, SrcPos, diff --git a/vhdl_ls/src/vhdl_server.rs b/vhdl_ls/src/vhdl_server.rs index 7f3c5dcc9..f45ea2268 100644 --- a/vhdl_ls/src/vhdl_server.rs +++ b/vhdl_ls/src/vhdl_server.rs @@ -22,7 +22,7 @@ use std::io; use std::io::ErrorKind; use std::path::{Path, PathBuf}; use vhdl_lang::{ - AnyEntKind, Concurrent, Config, EntHierarchy, EntRef, Message, MessageHandler, Object, + AnyEntKind, Case, Concurrent, Config, EntHierarchy, EntRef, Message, MessageHandler, Object, Overloaded, Project, SeverityMap, SrcPos, Token, Type, VHDLStandard, }; @@ -66,6 +66,7 @@ pub struct VHDLServer { init_params: Option, config_file: Option, severity_map: SeverityMap, + case_transform: Option, string_matcher: SkimMatcherV2, } @@ -81,6 +82,7 @@ impl VHDLServer { config_file: None, severity_map: SeverityMap::default(), string_matcher: SkimMatcherV2::default().use_cache(true).ignore_case(), + case_transform: None, } } @@ -96,6 +98,7 @@ impl VHDLServer { config_file: None, severity_map: SeverityMap::default(), string_matcher: SkimMatcherV2::default(), + case_transform: None, } } diff --git a/vhdl_ls/src/vhdl_server/completion.rs b/vhdl_ls/src/vhdl_server/completion.rs index e1d2600ff..091f25a87 100644 --- a/vhdl_ls/src/vhdl_server/completion.rs +++ b/vhdl_ls/src/vhdl_server/completion.rs @@ -7,74 +7,78 @@ use vhdl_lang::ast::{Designator, ObjectClass}; use vhdl_lang::{kind_str, AnyEntKind, Design, EntRef, InterfaceEnt, Overloaded}; impl VHDLServer { - fn completion_item_to_lsp_item( - &self, - item: vhdl_lang::CompletionItem, - ) -> lsp_types::CompletionItem { + fn insert_text(&self, val: impl ToString) -> String { + let mut val = val.to_string(); + if let Some(case) = &self.case_transform { + case.convert(&mut val) + } + val + } + + fn completion_item_to_lsp_item(&self, item: vhdl_lang::CompletionItem) -> CompletionItem { match item { - vhdl_lang::CompletionItem::Simple(ent) => entity_to_completion_item(ent), + vhdl_lang::CompletionItem::Simple(ent) => self.entity_to_completion_item(ent), vhdl_lang::CompletionItem::Work => CompletionItem { - label: "work".to_string(), + label: self.insert_text("work"), detail: Some("work library".to_string()), kind: Some(CompletionItemKind::MODULE), - insert_text: Some("work".to_string()), ..Default::default() }, vhdl_lang::CompletionItem::Formal(ent) => { - let mut item = entity_to_completion_item(ent); + let mut item = self.entity_to_completion_item(ent); if self.client_supports_snippets() { item.insert_text_format = Some(InsertTextFormat::SNIPPET); item.insert_text = Some(format!("{} => $1,", item.insert_text.unwrap())); } item } - vhdl_lang::CompletionItem::Overloaded(desi, count) => CompletionItem { - label: desi.to_string(), - detail: Some(format!("+{count} overloaded")), - kind: match desi { + vhdl_lang::CompletionItem::Overloaded(desi, count) => { + let kind = match desi { Designator::Identifier(_) => Some(CompletionItemKind::FUNCTION), Designator::OperatorSymbol(_) => Some(CompletionItemKind::OPERATOR), _ => None, - }, - insert_text: Some(desi.to_string()), - ..Default::default() - }, + }; + CompletionItem { + label: self.insert_text(desi), + detail: Some(format!("+{count} overloaded")), + kind, + ..Default::default() + } + } vhdl_lang::CompletionItem::Keyword(kind) => CompletionItem { - label: kind_str(kind).to_string(), + label: self.insert_text(kind_str(kind)), detail: Some(kind_str(kind).to_string()), - insert_text: Some(kind_str(kind).to_string()), kind: Some(CompletionItemKind::KEYWORD), ..Default::default() }, vhdl_lang::CompletionItem::Instantiation(ent, architectures) => { - let work_name = "work"; + let work_name = self.insert_text("work"); let library_names = if let Some(lib_name) = ent.library_name() { - vec![work_name.to_string(), lib_name.name().to_string()] + vec![work_name, self.insert_text(lib_name.name())] } else { - vec![work_name.to_string()] + vec![work_name] }; let (region, is_component_instantiation) = match ent.kind() { AnyEntKind::Design(Design::Entity(_, region)) => (region, false), AnyEntKind::Component(region) => (region, true), // should never happen but better return some value instead of crashing - _ => return entity_to_completion_item(ent), + _ => return self.entity_to_completion_item(ent), }; + let designator = self.insert_text(&ent.designator); let template = if self.client_supports_snippets() { let mut line = if is_component_instantiation { - format!("${{1:{}_inst}}: {}", ent.designator, ent.designator) + format!("${{1:{designator}_inst}}: {designator}",) } else { format!( - "${{1:{}_inst}}: entity ${{2|{}|}}.{}", - ent.designator, + "${{1:{designator}_inst}}: entity ${{2|{}|}}.{designator}", library_names.join(","), - ent.designator ) }; if architectures.len() > 1 { line.push_str("(${3|"); for (i, architecture) in architectures.iter().enumerate() { - line.push_str(&architecture.designator().to_string()); + line.push_str(&self.insert_text(architecture.designator())); if i != architectures.len() - 1 { line.push(',') } @@ -84,11 +88,11 @@ impl VHDLServer { let (ports, generics) = region.ports_and_generics(); let mut idx = 4; let mut interface_ent = |elements: Vec, purpose: &str| { - line += &*format!("\n {purpose} map(\n"); + line += &*format!("\n {purpose} {}(\n", self.insert_text("map")); for (i, generic) in elements.iter().enumerate() { + let generic_designator = self.insert_text(&generic.designator); line += &*format!( - " {} => ${{{}:{}}}", - generic.designator, idx, generic.designator + " {generic_designator} => ${{{idx}:{generic_designator}}}", ); idx += 1; if i != elements.len() - 1 { @@ -99,18 +103,18 @@ impl VHDLServer { line += ")"; }; if !generics.is_empty() { - interface_ent(generics, "generic"); + interface_ent(generics, &self.insert_text("generic")); } if !ports.is_empty() { - interface_ent(ports, "port"); + interface_ent(ports, &self.insert_text("port")); } line += ";"; line } else { - format!("{}", ent.designator) + designator.clone() }; CompletionItem { - label: format!("{} instantiation", ent.designator), + label: format!("{designator} instantiation"), insert_text: Some(template), insert_text_format: Some(InsertTextFormat::SNIPPET), kind: Some(CompletionItemKind::MODULE), @@ -118,9 +122,7 @@ impl VHDLServer { } } vhdl_lang::CompletionItem::Attribute(attribute) => CompletionItem { - label: format!("{attribute}"), - detail: Some(format!("{attribute}")), - insert_text: Some(format!("{attribute}")), + label: self.insert_text(attribute), kind: Some(CompletionItemKind::REFERENCE), ..Default::default() }, @@ -177,16 +179,15 @@ impl VHDLServer { } params } -} -fn entity_to_completion_item(ent: EntRef) -> CompletionItem { - CompletionItem { - label: ent.designator.to_string(), - detail: Some(ent.describe()), - kind: Some(entity_kind_to_completion_kind(ent.kind())), - data: serde_json::to_value(ent.id.to_raw()).ok(), - insert_text: Some(ent.designator.to_string()), - ..Default::default() + fn entity_to_completion_item(&self, ent: EntRef) -> CompletionItem { + CompletionItem { + label: self.insert_text(&ent.designator), + detail: Some(ent.describe()), + kind: Some(entity_kind_to_completion_kind(ent.kind())), + data: serde_json::to_value(ent.id.to_raw()).ok(), + ..Default::default() + } } } diff --git a/vhdl_ls/src/vhdl_server/lifecycle.rs b/vhdl_ls/src/vhdl_server/lifecycle.rs index ff0b0ce66..a48454ec3 100644 --- a/vhdl_ls/src/vhdl_server/lifecycle.rs +++ b/vhdl_ls/src/vhdl_server/lifecycle.rs @@ -48,6 +48,7 @@ impl VHDLServer { pub fn initialize_request(&mut self, init_params: InitializeParams) -> InitializeResult { self.config_file = self.root_uri_config_file(&init_params); let config = self.load_config(); + self.case_transform = config.preferred_case(); self.severity_map = *config.severities(); self.project = Project::from_config(config, &mut self.message_filter()); self.project.enable_all_linters(); diff --git a/vhdl_ls/src/vhdl_server/workspace.rs b/vhdl_ls/src/vhdl_server/workspace.rs index b17da42e3..13e762cf1 100644 --- a/vhdl_ls/src/vhdl_server/workspace.rs +++ b/vhdl_ls/src/vhdl_server/workspace.rs @@ -22,6 +22,7 @@ impl VHDLServer { )); let config = self.load_config(); self.severity_map = *config.severities(); + self.case_transform = config.preferred_case(); self.project .update_config(config, &mut self.message_filter());