Skip to content

Commit 4f20cbd

Browse files
authored
Add support for customizable case transformation in completions (#399)
1 parent 6d4e645 commit 4f20cbd

File tree

6 files changed

+184
-48
lines changed

6 files changed

+184
-48
lines changed

vhdl_lang/src/config.rs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use std::fs::File;
1212
use std::io;
1313
use std::io::prelude::*;
1414
use std::path::Path;
15+
use std::str::FromStr;
1516

1617
use fnv::FnvHashMap;
1718
use subst::VariableMap;
@@ -21,13 +22,124 @@ use crate::data::error_codes::ErrorCode;
2122
use crate::data::*;
2223
use crate::standard::VHDLStandard;
2324

25+
/// Defines standard VHDL case conventions.
26+
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
27+
pub enum Case {
28+
/// All lower case, i.e., `std_logic_vector`
29+
Lower,
30+
/// All upper-case, i.e., `STD_LOGIC_VECTOR`
31+
Upper,
32+
/// Pascal case, i.e., `Std_Logic_Vector`
33+
Pascal,
34+
}
35+
36+
impl FromStr for Case {
37+
type Err = String;
38+
39+
fn from_str(s: &str) -> Result<Self, Self::Err> {
40+
Ok(match s {
41+
"lower" | "snake" => Case::Lower,
42+
"upper" | "upper_snake" => Case::Upper,
43+
"pascal" | "upper_camel" => Case::Pascal,
44+
other => return Err(other.to_string()),
45+
})
46+
}
47+
}
48+
49+
impl Case {
50+
/// Converts the case in place, modifying the passed string.
51+
pub fn convert(&self, val: &mut str) {
52+
match self {
53+
Case::Lower => val.make_ascii_lowercase(),
54+
Case::Upper => val.make_ascii_uppercase(),
55+
Case::Pascal => {
56+
// SAFETY: changing ASCII letters only does not invalidate UTF-8.
57+
let bytes = unsafe { val.as_bytes_mut() };
58+
// First letter should be uppercased
59+
let mut next_uppercase = true;
60+
for byte in bytes {
61+
if byte == &b'_' {
62+
next_uppercase = true;
63+
continue;
64+
}
65+
if next_uppercase {
66+
byte.make_ascii_uppercase();
67+
} else {
68+
byte.make_ascii_lowercase();
69+
}
70+
next_uppercase = false;
71+
}
72+
}
73+
}
74+
}
75+
}
76+
77+
#[cfg(test)]
78+
mod case_tests {
79+
use super::*;
80+
81+
#[test]
82+
fn test_case_lower() {
83+
let mut test = String::from("STD_LOGIC_VECTOR");
84+
Case::Lower.convert(&mut test);
85+
assert_eq!(test, "std_logic_vector");
86+
}
87+
88+
#[test]
89+
fn test_case_upper() {
90+
let mut test = String::from("std_logic_vector");
91+
Case::Upper.convert(&mut test);
92+
assert_eq!(test, "STD_LOGIC_VECTOR");
93+
}
94+
95+
#[test]
96+
fn test_case_pascal() {
97+
let mut test = String::from("std_logic_vector");
98+
Case::Pascal.convert(&mut test);
99+
assert_eq!(test, "Std_Logic_Vector");
100+
}
101+
102+
#[test]
103+
fn test_case_empty() {
104+
for case in &[Case::Lower, Case::Upper, Case::Pascal] {
105+
let mut test = String::new();
106+
case.convert(&mut test);
107+
assert_eq!(test, "");
108+
}
109+
}
110+
111+
#[test]
112+
fn test_case_underscore_only() {
113+
for case in &[Case::Lower, Case::Upper, Case::Pascal] {
114+
let mut test = String::from("___");
115+
case.convert(&mut test);
116+
assert_eq!(test, "___");
117+
}
118+
}
119+
120+
#[test]
121+
fn test_case_consecutive_underscore() {
122+
let mut test = String::from("std__logic___vector");
123+
Case::Pascal.convert(&mut test);
124+
assert_eq!(test, "Std__Logic___Vector");
125+
}
126+
127+
#[test]
128+
fn test_case_mixed() {
129+
let mut test = String::from("StD_LoGiC_VeCToR");
130+
Case::Pascal.convert(&mut test);
131+
assert_eq!(test, "Std_Logic_Vector");
132+
}
133+
}
134+
24135
#[derive(Clone, PartialEq, Eq, Default, Debug)]
25136
pub struct Config {
26137
// A map from library name to file name
27138
libraries: FnvHashMap<String, LibraryConfig>,
28139
standard: VHDLStandard,
29140
// Defines the severity that diagnostics are displayed with
30141
severities: SeverityMap,
142+
preferred_case: Option<Case>,
31143
}
32144

33145
#[derive(Clone, PartialEq, Eq, Default, Debug)]
@@ -126,10 +238,22 @@ impl Config {
126238
SeverityMap::default()
127239
};
128240

241+
let case = if let Some(case) = config.get("preferred_case") {
242+
Some(
243+
case.as_str()
244+
.ok_or("preferred_case must be a string")?
245+
.parse()
246+
.map_err(|other| format!("Case '{other}' not valid"))?,
247+
)
248+
} else {
249+
None
250+
};
251+
129252
Ok(Config {
130253
libraries,
131254
severities,
132255
standard,
256+
preferred_case: case,
133257
})
134258
}
135259

@@ -192,6 +316,7 @@ impl Config {
192316
}
193317
}
194318
self.severities = config.severities;
319+
self.preferred_case = config.preferred_case;
195320
}
196321

197322
/// Load configuration file from installation folder
@@ -301,6 +426,11 @@ impl Config {
301426
pub fn standard(&self) -> VHDLStandard {
302427
self.standard
303428
}
429+
430+
/// Returns the casing that is preferred by the user for linting or completions.
431+
pub fn preferred_case(&self) -> Option<Case> {
432+
self.preferred_case
433+
}
304434
}
305435

306436
fn match_file_patterns(

vhdl_lang/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ mod completion;
2727
mod formatting;
2828
mod standard;
2929

30-
pub use crate::config::Config;
30+
pub use crate::config::{Case, Config};
3131
pub use crate::data::{
3232
Diagnostic, Latin1String, Message, MessageHandler, MessagePrinter, MessageType,
3333
NullDiagnostics, NullMessages, Position, Range, Severity, SeverityMap, Source, SrcPos,

vhdl_ls/src/vhdl_server.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use std::io;
2222
use std::io::ErrorKind;
2323
use std::path::{Path, PathBuf};
2424
use vhdl_lang::{
25-
AnyEntKind, Concurrent, Config, EntHierarchy, EntRef, Message, MessageHandler, Object,
25+
AnyEntKind, Case, Concurrent, Config, EntHierarchy, EntRef, Message, MessageHandler, Object,
2626
Overloaded, Project, SeverityMap, SrcPos, Token, Type, VHDLStandard,
2727
};
2828

@@ -66,6 +66,7 @@ pub struct VHDLServer {
6666
init_params: Option<InitializeParams>,
6767
config_file: Option<PathBuf>,
6868
severity_map: SeverityMap,
69+
case_transform: Option<Case>,
6970
string_matcher: SkimMatcherV2,
7071
}
7172

@@ -81,6 +82,7 @@ impl VHDLServer {
8182
config_file: None,
8283
severity_map: SeverityMap::default(),
8384
string_matcher: SkimMatcherV2::default().use_cache(true).ignore_case(),
85+
case_transform: None,
8486
}
8587
}
8688

@@ -96,6 +98,7 @@ impl VHDLServer {
9698
config_file: None,
9799
severity_map: SeverityMap::default(),
98100
string_matcher: SkimMatcherV2::default(),
101+
case_transform: None,
99102
}
100103
}
101104

vhdl_ls/src/vhdl_server/completion.rs

Lines changed: 47 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,74 +7,78 @@ use vhdl_lang::ast::{Designator, ObjectClass};
77
use vhdl_lang::{kind_str, AnyEntKind, Design, EntRef, InterfaceEnt, Overloaded};
88

99
impl VHDLServer {
10-
fn completion_item_to_lsp_item(
11-
&self,
12-
item: vhdl_lang::CompletionItem,
13-
) -> lsp_types::CompletionItem {
10+
fn insert_text(&self, val: impl ToString) -> String {
11+
let mut val = val.to_string();
12+
if let Some(case) = &self.case_transform {
13+
case.convert(&mut val)
14+
}
15+
val
16+
}
17+
18+
fn completion_item_to_lsp_item(&self, item: vhdl_lang::CompletionItem) -> CompletionItem {
1419
match item {
15-
vhdl_lang::CompletionItem::Simple(ent) => entity_to_completion_item(ent),
20+
vhdl_lang::CompletionItem::Simple(ent) => self.entity_to_completion_item(ent),
1621
vhdl_lang::CompletionItem::Work => CompletionItem {
17-
label: "work".to_string(),
22+
label: self.insert_text("work"),
1823
detail: Some("work library".to_string()),
1924
kind: Some(CompletionItemKind::MODULE),
20-
insert_text: Some("work".to_string()),
2125
..Default::default()
2226
},
2327
vhdl_lang::CompletionItem::Formal(ent) => {
24-
let mut item = entity_to_completion_item(ent);
28+
let mut item = self.entity_to_completion_item(ent);
2529
if self.client_supports_snippets() {
2630
item.insert_text_format = Some(InsertTextFormat::SNIPPET);
2731
item.insert_text = Some(format!("{} => $1,", item.insert_text.unwrap()));
2832
}
2933
item
3034
}
31-
vhdl_lang::CompletionItem::Overloaded(desi, count) => CompletionItem {
32-
label: desi.to_string(),
33-
detail: Some(format!("+{count} overloaded")),
34-
kind: match desi {
35+
vhdl_lang::CompletionItem::Overloaded(desi, count) => {
36+
let kind = match desi {
3537
Designator::Identifier(_) => Some(CompletionItemKind::FUNCTION),
3638
Designator::OperatorSymbol(_) => Some(CompletionItemKind::OPERATOR),
3739
_ => None,
38-
},
39-
insert_text: Some(desi.to_string()),
40-
..Default::default()
41-
},
40+
};
41+
CompletionItem {
42+
label: self.insert_text(desi),
43+
detail: Some(format!("+{count} overloaded")),
44+
kind,
45+
..Default::default()
46+
}
47+
}
4248
vhdl_lang::CompletionItem::Keyword(kind) => CompletionItem {
43-
label: kind_str(kind).to_string(),
49+
label: self.insert_text(kind_str(kind)),
4450
detail: Some(kind_str(kind).to_string()),
45-
insert_text: Some(kind_str(kind).to_string()),
4651
kind: Some(CompletionItemKind::KEYWORD),
4752
..Default::default()
4853
},
4954
vhdl_lang::CompletionItem::Instantiation(ent, architectures) => {
50-
let work_name = "work";
55+
let work_name = self.insert_text("work");
5156

5257
let library_names = if let Some(lib_name) = ent.library_name() {
53-
vec![work_name.to_string(), lib_name.name().to_string()]
58+
vec![work_name, self.insert_text(lib_name.name())]
5459
} else {
55-
vec![work_name.to_string()]
60+
vec![work_name]
5661
};
5762
let (region, is_component_instantiation) = match ent.kind() {
5863
AnyEntKind::Design(Design::Entity(_, region)) => (region, false),
5964
AnyEntKind::Component(region) => (region, true),
6065
// should never happen but better return some value instead of crashing
61-
_ => return entity_to_completion_item(ent),
66+
_ => return self.entity_to_completion_item(ent),
6267
};
68+
let designator = self.insert_text(&ent.designator);
6369
let template = if self.client_supports_snippets() {
6470
let mut line = if is_component_instantiation {
65-
format!("${{1:{}_inst}}: {}", ent.designator, ent.designator)
71+
format!("${{1:{designator}_inst}}: {designator}",)
6672
} else {
6773
format!(
68-
"${{1:{}_inst}}: entity ${{2|{}|}}.{}",
69-
ent.designator,
74+
"${{1:{designator}_inst}}: entity ${{2|{}|}}.{designator}",
7075
library_names.join(","),
71-
ent.designator
7276
)
7377
};
7478
if architectures.len() > 1 {
7579
line.push_str("(${3|");
7680
for (i, architecture) in architectures.iter().enumerate() {
77-
line.push_str(&architecture.designator().to_string());
81+
line.push_str(&self.insert_text(architecture.designator()));
7882
if i != architectures.len() - 1 {
7983
line.push(',')
8084
}
@@ -84,11 +88,11 @@ impl VHDLServer {
8488
let (ports, generics) = region.ports_and_generics();
8589
let mut idx = 4;
8690
let mut interface_ent = |elements: Vec<InterfaceEnt>, purpose: &str| {
87-
line += &*format!("\n {purpose} map(\n");
91+
line += &*format!("\n {purpose} {}(\n", self.insert_text("map"));
8892
for (i, generic) in elements.iter().enumerate() {
93+
let generic_designator = self.insert_text(&generic.designator);
8994
line += &*format!(
90-
" {} => ${{{}:{}}}",
91-
generic.designator, idx, generic.designator
95+
" {generic_designator} => ${{{idx}:{generic_designator}}}",
9296
);
9397
idx += 1;
9498
if i != elements.len() - 1 {
@@ -99,28 +103,26 @@ impl VHDLServer {
99103
line += ")";
100104
};
101105
if !generics.is_empty() {
102-
interface_ent(generics, "generic");
106+
interface_ent(generics, &self.insert_text("generic"));
103107
}
104108
if !ports.is_empty() {
105-
interface_ent(ports, "port");
109+
interface_ent(ports, &self.insert_text("port"));
106110
}
107111
line += ";";
108112
line
109113
} else {
110-
format!("{}", ent.designator)
114+
designator.clone()
111115
};
112116
CompletionItem {
113-
label: format!("{} instantiation", ent.designator),
117+
label: format!("{designator} instantiation"),
114118
insert_text: Some(template),
115119
insert_text_format: Some(InsertTextFormat::SNIPPET),
116120
kind: Some(CompletionItemKind::MODULE),
117121
..Default::default()
118122
}
119123
}
120124
vhdl_lang::CompletionItem::Attribute(attribute) => CompletionItem {
121-
label: format!("{attribute}"),
122-
detail: Some(format!("{attribute}")),
123-
insert_text: Some(format!("{attribute}")),
125+
label: self.insert_text(attribute),
124126
kind: Some(CompletionItemKind::REFERENCE),
125127
..Default::default()
126128
},
@@ -177,16 +179,15 @@ impl VHDLServer {
177179
}
178180
params
179181
}
180-
}
181182

182-
fn entity_to_completion_item(ent: EntRef) -> CompletionItem {
183-
CompletionItem {
184-
label: ent.designator.to_string(),
185-
detail: Some(ent.describe()),
186-
kind: Some(entity_kind_to_completion_kind(ent.kind())),
187-
data: serde_json::to_value(ent.id.to_raw()).ok(),
188-
insert_text: Some(ent.designator.to_string()),
189-
..Default::default()
183+
fn entity_to_completion_item(&self, ent: EntRef) -> CompletionItem {
184+
CompletionItem {
185+
label: self.insert_text(&ent.designator),
186+
detail: Some(ent.describe()),
187+
kind: Some(entity_kind_to_completion_kind(ent.kind())),
188+
data: serde_json::to_value(ent.id.to_raw()).ok(),
189+
..Default::default()
190+
}
190191
}
191192
}
192193

0 commit comments

Comments
 (0)