use lsp_types::Url;
use rustc_hash::FxHashMap;
use serde::{Deserialize, Deserializer};
use shuck_config::{FormatConfig, LintConfig, ShuckConfig};
use crate::session::settings::GlobalClientSettings;
use crate::{Client, logging};
pub(crate) type WorkspaceOptionsMap = FxHashMap<Url, ClientOptions>;
#[derive(Debug, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct GlobalOptions {
#[serde(flatten)]
client: ClientOptions,
#[serde(default)]
pub(crate) tracing: TracingOptions,
}
impl GlobalOptions {
pub fn into_settings(self, client: Client) -> GlobalClientSettings {
GlobalClientSettings::new(self.client, client)
}
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClientOptions {
#[serde(default)]
pub lint: Option<LintConfig>,
#[serde(default)]
pub format: Option<FormatConfig>,
#[serde(default)]
pub fix_all: Option<bool>,
#[serde(default)]
pub unsafe_fixes: Option<bool>,
#[serde(default)]
pub show_syntax_errors: Option<bool>,
#[serde(default)]
pub server: ServerOptions,
}
impl ClientOptions {
pub(crate) fn to_config_overrides(&self) -> ShuckConfig {
ShuckConfig {
lint: self.lint.clone().unwrap_or_default(),
format: self.format.clone().unwrap_or_default(),
..ShuckConfig::default()
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ServerOptions {
pub workspace_symbols: WorkspaceSymbolFeatureOptions,
pub completion: CompletionFeatureOptions,
pub rename: RenameFeatureOptions,
pub call_hierarchy: CallHierarchyFeatureOptions,
pub workspace_diagnostics: WorkspaceDiagnosticsFeatureOptions,
workspace_symbols_overrides: WorkspaceSymbolFeatureOptionsOverrides,
completion_overrides: CompletionFeatureOptionsOverrides,
rename_overrides: RenameFeatureOptionsOverrides,
call_hierarchy_overrides: CallHierarchyFeatureOptionsOverrides,
workspace_diagnostics_overrides: WorkspaceDiagnosticsFeatureOptionsOverrides,
}
impl ServerOptions {
pub(crate) fn workspace_symbols_layered_over(
&self,
base: WorkspaceSymbolFeatureOptions,
) -> WorkspaceSymbolFeatureOptions {
if self.workspace_symbols_overrides.has_overrides() {
self.workspace_symbols_overrides.apply_to(base)
} else if self.workspace_symbols != WorkspaceSymbolFeatureOptions::default() {
self.workspace_symbols
} else {
base
}
}
pub(crate) fn completion_layered_over(
&self,
base: CompletionFeatureOptions,
) -> CompletionFeatureOptions {
if self.completion_overrides.has_overrides() {
self.completion_overrides.apply_to(base)
} else if self.completion != CompletionFeatureOptions::default() {
self.completion
} else {
base
}
}
pub(crate) fn rename_layered_over(&self, base: RenameFeatureOptions) -> RenameFeatureOptions {
if self.rename_overrides.has_overrides() {
self.rename_overrides.apply_to(base)
} else if self.rename != RenameFeatureOptions::default() {
self.rename
} else {
base
}
}
pub(crate) fn call_hierarchy_layered_over(
&self,
base: CallHierarchyFeatureOptions,
) -> CallHierarchyFeatureOptions {
if self.call_hierarchy_overrides.has_overrides() {
self.call_hierarchy_overrides.apply_to(base)
} else if self.call_hierarchy != CallHierarchyFeatureOptions::default() {
self.call_hierarchy
} else {
base
}
}
pub(crate) fn workspace_diagnostics_layered_over(
&self,
base: WorkspaceDiagnosticsFeatureOptions,
) -> WorkspaceDiagnosticsFeatureOptions {
if self.workspace_diagnostics_overrides.has_overrides() {
self.workspace_diagnostics_overrides.apply_to(base)
} else if self.workspace_diagnostics != WorkspaceDiagnosticsFeatureOptions::default() {
self.workspace_diagnostics
} else {
base
}
}
}
impl<'de> Deserialize<'de> for ServerOptions {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize, Default)]
#[serde(rename_all = "camelCase")]
struct RawServerOptions {
#[serde(default)]
workspace_symbols: WorkspaceSymbolFeatureOptionsOverrides,
#[serde(default)]
completion: CompletionFeatureOptionsOverrides,
#[serde(default)]
rename: RenameFeatureOptionsOverrides,
#[serde(default)]
call_hierarchy: CallHierarchyFeatureOptionsOverrides,
#[serde(default)]
workspace_diagnostics: WorkspaceDiagnosticsFeatureOptionsOverrides,
}
let raw = RawServerOptions::deserialize(deserializer)?;
Ok(Self {
workspace_symbols: raw
.workspace_symbols
.apply_to(WorkspaceSymbolFeatureOptions::default()),
completion: raw.completion.apply_to(CompletionFeatureOptions::default()),
rename: raw.rename.apply_to(RenameFeatureOptions::default()),
call_hierarchy: raw
.call_hierarchy
.apply_to(CallHierarchyFeatureOptions::default()),
workspace_diagnostics: raw
.workspace_diagnostics
.apply_to(WorkspaceDiagnosticsFeatureOptions::default()),
workspace_symbols_overrides: raw.workspace_symbols,
completion_overrides: raw.completion,
rename_overrides: raw.rename,
call_hierarchy_overrides: raw.call_hierarchy,
workspace_diagnostics_overrides: raw.workspace_diagnostics,
})
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
struct WorkspaceSymbolFeatureOptionsOverrides {
#[serde(default)]
enabled: Option<bool>,
#[serde(default)]
max_files: Option<usize>,
}
impl WorkspaceSymbolFeatureOptionsOverrides {
fn has_overrides(self) -> bool {
self.enabled.is_some() || self.max_files.is_some()
}
fn apply_to(self, base: WorkspaceSymbolFeatureOptions) -> WorkspaceSymbolFeatureOptions {
WorkspaceSymbolFeatureOptions {
enabled: self.enabled.unwrap_or(base.enabled),
max_files: self.max_files.unwrap_or(base.max_files),
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceSymbolFeatureOptions {
#[serde(default = "default_workspace_symbols_enabled")]
pub enabled: bool,
#[serde(default = "default_workspace_symbols_max_files")]
pub max_files: usize,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
struct CompletionFeatureOptionsOverrides {
#[serde(default)]
include_runtime_names: Option<bool>,
#[serde(default)]
include_keywords: Option<bool>,
}
impl CompletionFeatureOptionsOverrides {
fn has_overrides(self) -> bool {
self.include_runtime_names.is_some() || self.include_keywords.is_some()
}
fn apply_to(self, base: CompletionFeatureOptions) -> CompletionFeatureOptions {
CompletionFeatureOptions {
include_runtime_names: self
.include_runtime_names
.unwrap_or(base.include_runtime_names),
include_keywords: self.include_keywords.unwrap_or(base.include_keywords),
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CompletionFeatureOptions {
#[serde(default = "default_completion_include_runtime_names")]
pub include_runtime_names: bool,
#[serde(default = "default_completion_include_keywords")]
pub include_keywords: bool,
}
impl Default for CompletionFeatureOptions {
fn default() -> Self {
Self {
include_runtime_names: true,
include_keywords: true,
}
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
struct RenameFeatureOptionsOverrides {
#[serde(default)]
allow_cross_file: Option<bool>,
}
impl RenameFeatureOptionsOverrides {
fn has_overrides(self) -> bool {
self.allow_cross_file.is_some()
}
fn apply_to(self, base: RenameFeatureOptions) -> RenameFeatureOptions {
RenameFeatureOptions {
allow_cross_file: self.allow_cross_file.unwrap_or(base.allow_cross_file),
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct RenameFeatureOptions {
#[serde(default = "default_cross_file_rename_enabled")]
pub allow_cross_file: bool,
}
impl Default for RenameFeatureOptions {
fn default() -> Self {
Self {
allow_cross_file: default_cross_file_rename_enabled(),
}
}
}
fn default_cross_file_rename_enabled() -> bool {
true
}
impl Default for WorkspaceSymbolFeatureOptions {
fn default() -> Self {
Self {
enabled: true,
max_files: 5000,
}
}
}
fn default_workspace_symbols_enabled() -> bool {
true
}
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
struct CallHierarchyFeatureOptionsOverrides {
#[serde(default)]
max_files: Option<usize>,
}
impl CallHierarchyFeatureOptionsOverrides {
fn has_overrides(self) -> bool {
self.max_files.is_some()
}
fn apply_to(self, base: CallHierarchyFeatureOptions) -> CallHierarchyFeatureOptions {
CallHierarchyFeatureOptions {
max_files: self.max_files.unwrap_or(base.max_files),
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CallHierarchyFeatureOptions {
#[serde(default = "default_call_hierarchy_max_files")]
pub max_files: usize,
}
impl Default for CallHierarchyFeatureOptions {
fn default() -> Self {
Self {
max_files: default_call_hierarchy_max_files(),
}
}
}
fn default_call_hierarchy_max_files() -> usize {
10_000
}
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
struct WorkspaceDiagnosticsFeatureOptionsOverrides {
#[serde(default)]
enabled: Option<bool>,
#[serde(default)]
max_files: Option<usize>,
#[serde(default)]
max_entries: Option<usize>,
#[serde(default)]
max_source_bytes: Option<usize>,
}
impl WorkspaceDiagnosticsFeatureOptionsOverrides {
fn has_overrides(self) -> bool {
self.enabled.is_some()
|| self.max_files.is_some()
|| self.max_entries.is_some()
|| self.max_source_bytes.is_some()
}
fn apply_to(
self,
base: WorkspaceDiagnosticsFeatureOptions,
) -> WorkspaceDiagnosticsFeatureOptions {
WorkspaceDiagnosticsFeatureOptions {
enabled: self.enabled.unwrap_or(base.enabled),
max_files: self.max_files.unwrap_or(base.max_files),
max_entries: self.max_entries.unwrap_or(base.max_entries),
max_source_bytes: self.max_source_bytes.unwrap_or(base.max_source_bytes),
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceDiagnosticsFeatureOptions {
#[serde(default = "default_workspace_diagnostics_enabled")]
pub enabled: bool,
#[serde(default = "default_workspace_diagnostics_max_files")]
pub max_files: usize,
#[serde(default = "default_workspace_diagnostics_max_entries")]
pub max_entries: usize,
#[serde(default = "default_workspace_diagnostics_max_source_bytes")]
pub max_source_bytes: usize,
}
impl Default for WorkspaceDiagnosticsFeatureOptions {
fn default() -> Self {
Self {
enabled: default_workspace_diagnostics_enabled(),
max_files: default_workspace_diagnostics_max_files(),
max_entries: default_workspace_diagnostics_max_entries(),
max_source_bytes: default_workspace_diagnostics_max_source_bytes(),
}
}
}
fn default_workspace_diagnostics_enabled() -> bool {
true
}
fn default_workspace_diagnostics_max_files() -> usize {
1_000
}
fn default_workspace_diagnostics_max_entries() -> usize {
10_000
}
fn default_workspace_diagnostics_max_source_bytes() -> usize {
32 * 1024 * 1024
}
fn default_workspace_symbols_max_files() -> usize {
5000
}
fn default_completion_include_runtime_names() -> bool {
true
}
fn default_completion_include_keywords() -> bool {
true
}
#[derive(Debug, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub(crate) struct TracingOptions {
pub(crate) log_file: Option<std::path::PathBuf>,
pub(crate) log_level: Option<logging::LogLevel>,
}
#[derive(Debug, Default)]
pub(crate) struct AllOptions {
pub(crate) global: GlobalOptions,
pub(crate) workspace: Option<WorkspaceOptionsMap>,
}
#[derive(Debug, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
struct InitializationOptions {
#[serde(default)]
shuck: GlobalOptions,
#[serde(default)]
workspace: Option<WorkspaceOptionsMap>,
}
impl AllOptions {
pub(crate) fn from_value(value: serde_json::Value) -> Self {
if value
.as_object()
.is_some_and(|object| object.contains_key("shuck"))
{
let options =
serde_json::from_value::<InitializationOptions>(value).unwrap_or_default();
return Self {
global: options.shuck,
workspace: options.workspace,
};
}
let global = serde_json::from_value::<GlobalOptions>(value).unwrap_or_default();
Self {
global,
workspace: None,
}
}
pub(crate) fn workspace_diagnostics_enabled(&self) -> bool {
let global = self.global.client.server.workspace_diagnostics;
global.enabled
|| self.workspace.as_ref().is_some_and(|workspaces| {
workspaces.values().any(|options| {
options
.server
.workspace_diagnostics_layered_over(global)
.enabled
})
})
}
}
#[cfg(test)]
mod tests {
use super::AllOptions;
#[test]
fn workspace_diagnostics_are_enabled_by_default_and_can_be_disabled() {
let defaults = AllOptions::from_value(serde_json::json!({}));
assert!(defaults.workspace_diagnostics_enabled());
let disabled = AllOptions::from_value(serde_json::json!({
"shuck": {
"server": {
"workspaceDiagnostics": {
"enabled": false
}
}
}
}));
assert!(!disabled.workspace_diagnostics_enabled());
}
#[test]
fn unrelated_workspace_options_preserve_the_global_diagnostic_opt_out() {
let disabled = AllOptions::from_value(serde_json::json!({
"shuck": {
"server": {
"workspaceDiagnostics": {
"enabled": false
}
}
},
"workspace": {
"file:///workspace": {
"showSyntaxErrors": true
}
}
}));
assert!(!disabled.workspace_diagnostics_enabled());
let enabled_for_workspace = AllOptions::from_value(serde_json::json!({
"shuck": {
"server": {
"workspaceDiagnostics": {
"enabled": false
}
}
},
"workspace": {
"file:///workspace": {
"server": {
"workspaceDiagnostics": {
"enabled": true
}
}
}
}
}));
assert!(enabled_for_workspace.workspace_diagnostics_enabled());
}
}