Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions vhdl_lang/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,13 +22,124 @@ 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<Self, Self::Err> {
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
libraries: FnvHashMap<String, LibraryConfig>,
standard: VHDLStandard,
// Defines the severity that diagnostics are displayed with
severities: SeverityMap,
preferred_case: Option<Case>,
}

#[derive(Clone, PartialEq, Eq, Default, Debug)]
Expand Down Expand Up @@ -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,
})
}

Expand Down Expand Up @@ -192,6 +316,7 @@ impl Config {
}
}
self.severities = config.severities;
self.preferred_case = config.preferred_case;
}

/// Load configuration file from installation folder
Expand Down Expand Up @@ -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<Case> {
self.preferred_case
}
}

fn match_file_patterns(
Expand Down
2 changes: 1 addition & 1 deletion vhdl_lang/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion vhdl_ls/src/vhdl_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -66,6 +66,7 @@ pub struct VHDLServer {
init_params: Option<InitializeParams>,
config_file: Option<PathBuf>,
severity_map: SeverityMap,
case_transform: Option<Case>,
string_matcher: SkimMatcherV2,
}

Expand All @@ -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,
}
}

Expand All @@ -96,6 +98,7 @@ impl VHDLServer {
config_file: None,
severity_map: SeverityMap::default(),
string_matcher: SkimMatcherV2::default(),
case_transform: None,
}
}

Expand Down
93 changes: 47 additions & 46 deletions vhdl_ls/src/vhdl_server/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(',')
}
Expand All @@ -84,11 +88,11 @@ impl VHDLServer {
let (ports, generics) = region.ports_and_generics();
let mut idx = 4;
let mut interface_ent = |elements: Vec<InterfaceEnt>, 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 {
Expand All @@ -99,28 +103,26 @@ 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),
..Default::default()
}
}
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()
},
Expand Down Expand Up @@ -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()
}
}
}

Expand Down
Loading
Loading