diff --git a/helix-config/src/lib.rs b/helix-config/src/lib.rs index 336a57f1b..9fe65b7e4 100644 --- a/helix-config/src/lib.rs +++ b/helix-config/src/lib.rs @@ -14,6 +14,7 @@ use any::ConfigData; use convert::ty_into_value; pub use convert::IntoTy; pub use definition::{init_config, init_language_server_config}; +pub use toml::read_toml_config; use validator::StaticValidator; pub use validator::{regex_str_validator, ty_validator, IntegerRangeValidator, Ty, Validator}; pub use value::{from_value, to_value, Value}; @@ -23,6 +24,7 @@ mod convert; mod definition; pub mod env; mod macros; +mod toml; mod validator; mod value; diff --git a/helix-config/src/toml.rs b/helix-config/src/toml.rs new file mode 100644 index 000000000..e79d42acd --- /dev/null +++ b/helix-config/src/toml.rs @@ -0,0 +1,69 @@ +use crate::{Map, OptionManager, OptionRegistry, Value}; + +/// Inserts the config declaration from a map deserialized from toml into +/// options manager. Returns an error if any of theu config options are +/// invalid The convresion may not be exactly one-to-one to retain backwards +/// compatibility +pub fn read_toml_config( + config_entries: Map, + options: &OptionManager, + registry: &OptionRegistry, +) -> anyhow::Result<()> { + let mut buf = String::new(); + for (key, val) in config_entries { + if matches!(val, Value::Map(_)) { + buf.push_str(&key); + visit(&mut buf, val, options, registry)?; + buf.clear(); + } else { + visit(&mut key.to_string(), val, options, registry)?; + } + } + Ok(()) +} + +fn visit( + path: &mut String, + val: Value, + options: &OptionManager, + registry: &OptionRegistry, +) -> anyhow::Result<()> { + match &**path { + // don't descend + "auto-format" => { + // treat as unset + if Value::Bool(true) == val { + return Ok(()); + } + } + "auto-pairs" => return options.set("auto-pairs", val, registry), + "enviorment" => return options.set("enviorment", val, registry), + "config" => return options.set("config", val, registry), + "gutters" if matches!(val, Value::List(_)) => { + return options.set("gutters.layout", val, registry); + } + "gutters" if matches!(val, Value::List(_)) => { + return options.set("gutters.layout", val, registry); + } + "whitespace.render" if matches!(val, Value::String(_)) => { + return options.set("whitespace.render.default", val, registry); + } + "language-servers" => { + // merge list/map of language servers but if "only" and "except" are specified overwrite + return options.append("language-servers", val, registry, 0); + } + _ => (), + }; + if let Value::Map(val) = val { + let old_path_len = path.len(); + for (key, val) in val.into_iter() { + path.push('.'); + path.push_str(&key); + visit(path, val, options, registry)?; + path.truncate(old_path_len); + } + Ok(()) + } else { + options.set(&**path, val, registry) + } +} diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 255c9a63b..73ea7ea82 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -84,421 +84,8 @@ pub struct Configuration { impl Default for Configuration { fn default() -> Self { - crate::config::default_syntax_loader() - } -} - -// largely based on tree-sitter/cli/src/loader.rs -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case", deny_unknown_fields)] -pub struct LanguageConfiguration { - #[serde(rename = "name")] - pub language_id: String, // c-sharp, rust, tsx - #[serde(rename = "language-id")] - // see the table under https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem - pub language_server_language_id: Option, // csharp, rust, typescriptreact, for the language-server - pub file_types: Vec, // filename extension or ends_with? - #[serde(default)] - pub shebangs: Vec, // interpreter(s) associated with language - #[serde(default)] - pub roots: Vec, // these indicate project roots <.git, Cargo.toml> - pub comment_token: Option, - pub text_width: Option, - pub soft_wrap: Option, - - #[serde(default)] - pub auto_format: bool, - - #[serde(skip_serializing_if = "Option::is_none")] - pub formatter: Option, - - #[serde(default)] - pub diagnostic_severity: Severity, - - pub grammar: Option, // tree-sitter grammar name, defaults to language_id - - // content_regex - #[serde(default, skip_serializing, deserialize_with = "deserialize_regex")] - pub injection_regex: Option, - // first_line_regex - // - #[serde(skip)] - pub(crate) highlight_config: OnceCell>>, - // tags_config OnceCell<> https://github.com/tree-sitter/tree-sitter/pull/583 - #[serde( - default, - skip_serializing_if = "Vec::is_empty", - serialize_with = "serialize_lang_features", - deserialize_with = "deserialize_lang_features" - )] - pub language_servers: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub indent: Option, - - #[serde(skip)] - pub(crate) indent_query: OnceCell>, - #[serde(skip)] - pub(crate) textobject_query: OnceCell>, - #[serde(skip_serializing_if = "Option::is_none")] - pub debugger: Option, - - /// Automatic insertion of pairs to parentheses, brackets, - /// etc. Defaults to true. Optionally, this can be a list of 2-tuples - /// to specify a list of characters to pair. This overrides the - /// global setting. - #[serde(default, skip_serializing, deserialize_with = "deserialize_auto_pairs")] - pub auto_pairs: Option, - - pub rulers: Option>, // if set, override editor's rulers - - /// Hardcoded LSP root directories relative to the workspace root, like `examples` or `tools/fuzz`. - /// Falling back to the current working directory if none are configured. - pub workspace_lsp_roots: Option>, - #[serde(default)] - pub persistent_diagnostic_sources: Vec, -} - -#[derive(Debug, PartialEq, Eq, Hash)] -pub enum FileType { - /// The extension of the file, either the `Path::extension` or the full - /// filename if the file does not have an extension. - Extension(String), - /// The suffix of a file. This is compared to a given file's absolute - /// path, so it can be used to detect files based on their directories. - Suffix(String), -} - -impl Serialize for FileType { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - use serde::ser::SerializeMap; - - match self { - FileType::Extension(extension) => serializer.serialize_str(extension), - FileType::Suffix(suffix) => { - let mut map = serializer.serialize_map(Some(1))?; - map.serialize_entry("suffix", &suffix.replace(std::path::MAIN_SEPARATOR, "/"))?; - map.end() - } - } - } -} - -impl<'de> Deserialize<'de> for FileType { - fn deserialize(deserializer: D) -> Result - where - D: serde::de::Deserializer<'de>, - { - struct FileTypeVisitor; - - impl<'de> serde::de::Visitor<'de> for FileTypeVisitor { - type Value = FileType; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("string or table") - } - - fn visit_str(self, value: &str) -> Result - where - E: serde::de::Error, - { - Ok(FileType::Extension(value.to_string())) - } - - fn visit_map(self, mut map: M) -> Result - where - M: serde::de::MapAccess<'de>, - { - match map.next_entry::()? { - Some((key, suffix)) if key == "suffix" => Ok(FileType::Suffix({ - suffix.replace('/', std::path::MAIN_SEPARATOR_STR) - })), - Some((key, _value)) => Err(serde::de::Error::custom(format!( - "unknown key in `file-types` list: {}", - key - ))), - None => Err(serde::de::Error::custom( - "expected a `suffix` key in the `file-types` entry", - )), - } - } - } - - deserializer.deserialize_any(FileTypeVisitor) - } -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] -#[serde(rename_all = "kebab-case")] -pub enum LanguageServerFeature { - Format, - GotoDeclaration, - GotoDefinition, - GotoTypeDefinition, - GotoReference, - GotoImplementation, - // Goto, use bitflags, combining previous Goto members? - SignatureHelp, - Hover, - DocumentHighlight, - Completion, - CodeAction, - WorkspaceCommand, - DocumentSymbols, - WorkspaceSymbols, - // Symbols, use bitflags, see above? - Diagnostics, - RenameSymbol, - InlayHints, -} - -impl Display for LanguageServerFeature { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use LanguageServerFeature::*; - let feature = match self { - Format => "format", - GotoDeclaration => "goto-declaration", - GotoDefinition => "goto-definition", - GotoTypeDefinition => "goto-type-definition", - GotoReference => "goto-type-definition", - GotoImplementation => "goto-implementation", - SignatureHelp => "signature-help", - Hover => "hover", - DocumentHighlight => "document-highlight", - Completion => "completion", - CodeAction => "code-action", - WorkspaceCommand => "workspace-command", - DocumentSymbols => "document-symbols", - WorkspaceSymbols => "workspace-symbols", - Diagnostics => "diagnostics", - RenameSymbol => "rename-symbol", - InlayHints => "inlay-hints", - }; - write!(f, "{feature}",) - } -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(untagged, rename_all = "kebab-case", deny_unknown_fields)] -enum LanguageServerFeatureConfiguration { - #[serde(rename_all = "kebab-case")] - Features { - #[serde(default, skip_serializing_if = "HashSet::is_empty")] - only_features: HashSet, - #[serde(default, skip_serializing_if = "HashSet::is_empty")] - except_features: HashSet, - name: String, - }, - Simple(String), -} - -#[derive(Debug, Default)] -pub struct LanguageServerFeatures { - pub name: String, - pub only: HashSet, - pub excluded: HashSet, -} - -impl LanguageServerFeatures { - pub fn has_feature(&self, feature: LanguageServerFeature) -> bool { - (self.only.is_empty() || self.only.contains(&feature)) && !self.excluded.contains(&feature) - } -} - -fn deserialize_lang_features<'de, D>( - deserializer: D, -) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - let raw: Vec = Deserialize::deserialize(deserializer)?; - let res = raw - .into_iter() - .map(|config| match config { - LanguageServerFeatureConfiguration::Simple(name) => LanguageServerFeatures { - name, - ..Default::default() - }, - LanguageServerFeatureConfiguration::Features { - only_features, - except_features, - name, - } => LanguageServerFeatures { - name, - only: only_features, - excluded: except_features, - }, - }) - .collect(); - Ok(res) -} -fn serialize_lang_features( - map: &Vec, - serializer: S, -) -> Result -where - S: serde::Serializer, -{ - let mut serializer = serializer.serialize_seq(Some(map.len()))?; - for features in map { - let features = if features.only.is_empty() && features.excluded.is_empty() { - LanguageServerFeatureConfiguration::Simple(features.name.to_owned()) - } else { - LanguageServerFeatureConfiguration::Features { - only_features: features.only.clone(), - except_features: features.excluded.clone(), - name: features.name.to_owned(), - } - }; - serializer.serialize_element(&features)?; - } - serializer.end() -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct LanguageServerConfiguration { - pub command: String, - #[serde(default)] - #[serde(skip_serializing_if = "Vec::is_empty")] - pub args: Vec, - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub environment: HashMap, - #[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")] - pub config: Option, - #[serde(default = "default_timeout")] - pub timeout: u64, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct FormatterConfiguration { - pub command: String, - #[serde(default)] - #[serde(skip_serializing_if = "Vec::is_empty")] - pub args: Vec, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct AdvancedCompletion { - pub name: Option, - pub completion: Option, - pub default: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case", untagged)] -pub enum DebugConfigCompletion { - Named(String), - Advanced(AdvancedCompletion), -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(untagged)] -pub enum DebugArgumentValue { - String(String), - Array(Vec), - Boolean(bool), -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct DebugTemplate { - pub name: String, - pub request: String, - pub completion: Vec, - pub args: HashMap, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct DebugAdapterConfig { - pub name: String, - pub transport: String, - #[serde(default)] - pub command: String, - #[serde(default)] - pub args: Vec, - pub port_arg: Option, - pub templates: Vec, - #[serde(default)] - pub quirks: DebuggerQuirks, -} - -// Different workarounds for adapters' differences -#[derive(Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct DebuggerQuirks { - #[serde(default)] - pub absolute_paths: bool, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct IndentationConfiguration { - #[serde(deserialize_with = "deserialize_tab_width")] - pub tab_width: usize, - pub unit: String, -} - -/// How the indentation for a newly inserted line should be determined. -/// If the selected heuristic is not available (e.g. because the current -/// language has no tree-sitter indent queries), a simpler one will be used. -#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum IndentationHeuristic { - /// Just copy the indentation of the line that the cursor is currently on. - Simple, - /// Use tree-sitter indent queries to compute the expected absolute indentation level of the new line. - TreeSitter, - /// Use tree-sitter indent queries to compute the expected difference in indentation between the new line - /// and the line before. Add this to the actual indentation level of the line before. - #[default] - Hybrid, -} - -/// Configuration for auto pairs -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case", deny_unknown_fields, untagged)] -pub enum AutoPairConfig { - /// Enables or disables auto pairing. False means disabled. True means to use the default pairs. - Enable(bool), - - /// The mappings of pairs. - Pairs(HashMap), -} - -impl Default for AutoPairConfig { - fn default() -> Self { - AutoPairConfig::Enable(true) - } -} - -impl From<&AutoPairConfig> for Option { - fn from(auto_pair_config: &AutoPairConfig) -> Self { - match auto_pair_config { - AutoPairConfig::Enable(false) => None, - AutoPairConfig::Enable(true) => Some(AutoPairs::default()), - AutoPairConfig::Pairs(pairs) => Some(AutoPairs::new(pairs.iter())), - } - } -} - -impl From for Option { - fn from(auto_pairs_config: AutoPairConfig) -> Self { - (&auto_pairs_config).into() - } -} - -impl FromStr for AutoPairConfig { - type Err = std::str::ParseBoolError; - - // only do bool parsing for runtime setting - fn from_str(s: &str) -> Result { - let enable: bool = s.parse()?; - Ok(AutoPairConfig::Enable(enable)) + todo!() + // crate::config::default_syntax_loader() } } @@ -717,6 +304,7 @@ impl LanguageConfiguration { .ok() } } + #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(default, rename_all = "kebab-case", deny_unknown_fields)] pub struct SoftWrap { diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index ebf261f64..be383d912 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -41,833 +41,13 @@ use anyhow::{anyhow, bail, Error}; pub use helix_core::diagnostic::Severity; use helix_core::{ - auto_pairs::AutoPairs, - syntax::{self, AutoPairConfig, IndentationHeuristic, LanguageServerFeature, SoftWrap}, - Change, LineEnding, Position, Selection, NATIVE_LINE_ENDING, + syntax::{self, LanguageServerFeature}, + Change, Position, Selection, }; use helix_dap as dap; use helix_lsp::lsp; -use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; - -use arc_swap::access::{DynAccess, DynGuard}; - -fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result -where - D: serde::Deserializer<'de>, -{ - let millis = u64::deserialize(deserializer)?; - Ok(Duration::from_millis(millis)) -} - -fn serialize_duration_millis(duration: &Duration, serializer: S) -> Result -where - S: Serializer, -{ - serializer.serialize_u64( - duration - .as_millis() - .try_into() - .map_err(|_| serde::ser::Error::custom("duration value overflowed u64"))?, - ) -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case", default, deny_unknown_fields)] -pub struct GutterConfig { - /// Gutter Layout - pub layout: Vec, - /// Options specific to the "line-numbers" gutter - pub line_numbers: GutterLineNumbersConfig, -} - -impl Default for GutterConfig { - fn default() -> Self { - Self { - layout: vec![ - GutterType::Diagnostics, - GutterType::Spacer, - GutterType::LineNumbers, - GutterType::Spacer, - GutterType::Diff, - ], - line_numbers: GutterLineNumbersConfig::default(), - } - } -} - -impl From> for GutterConfig { - fn from(x: Vec) -> Self { - GutterConfig { - layout: x, - ..Default::default() - } - } -} - -fn deserialize_gutter_seq_or_struct<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - struct GutterVisitor; - - impl<'de> serde::de::Visitor<'de> for GutterVisitor { - type Value = GutterConfig; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - formatter, - "an array of gutter names or a detailed gutter configuration" - ) - } - - fn visit_seq(self, mut seq: S) -> Result - where - S: serde::de::SeqAccess<'de>, - { - let mut gutters = Vec::new(); - while let Some(gutter) = seq.next_element::()? { - gutters.push( - gutter - .parse::() - .map_err(serde::de::Error::custom)?, - ) - } - - Ok(gutters.into()) - } - - fn visit_map(self, map: M) -> Result - where - M: serde::de::MapAccess<'de>, - { - let deserializer = serde::de::value::MapAccessDeserializer::new(map); - Deserialize::deserialize(deserializer) - } - } - - deserializer.deserialize_any(GutterVisitor) -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case", default, deny_unknown_fields)] -pub struct GutterLineNumbersConfig { - /// Minimum number of characters to use for line number gutter. Defaults to 3. - pub min_width: usize, -} - -impl Default for GutterLineNumbersConfig { - fn default() -> Self { - Self { min_width: 3 } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case", default, deny_unknown_fields)] -pub struct FilePickerConfig { - /// IgnoreOptions - /// Enables ignoring hidden files. - /// Whether to hide hidden files in file picker and global search results. Defaults to true. - pub hidden: bool, - /// Enables following symlinks. - /// Whether to follow symbolic links in file picker and file or directory completions. Defaults to true. - pub follow_symlinks: bool, - /// Hides symlinks that point into the current directory. Defaults to true. - pub deduplicate_links: bool, - /// Enables reading ignore files from parent directories. Defaults to true. - pub parents: bool, - /// Enables reading `.ignore` files. - /// Whether to hide files listed in .ignore in file picker and global search results. Defaults to true. - pub ignore: bool, - /// Enables reading `.gitignore` files. - /// Whether to hide files listed in .gitignore in file picker and global search results. Defaults to true. - pub git_ignore: bool, - /// Enables reading global .gitignore, whose path is specified in git's config: `core.excludefile` option. - /// Whether to hide files listed in global .gitignore in file picker and global search results. Defaults to true. - pub git_global: bool, - /// Enables reading `.git/info/exclude` files. - /// Whether to hide files listed in .git/info/exclude in file picker and global search results. Defaults to true. - pub git_exclude: bool, - /// WalkBuilder options - /// Maximum Depth to recurse directories in file picker and global search. Defaults to `None`. - pub max_depth: Option, -} - -impl Default for FilePickerConfig { - fn default() -> Self { - Self { - hidden: true, - follow_symlinks: true, - deduplicate_links: true, - parents: true, - ignore: true, - git_ignore: true, - git_global: true, - git_exclude: true, - max_depth: None, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case", default, deny_unknown_fields)] -pub struct Config { - /// Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5. - pub scrolloff: usize, - /// Number of lines to scroll at once. Defaults to 3 - pub scroll_lines: isize, - /// Mouse support. Defaults to true. - pub mouse: bool, - /// Shell to use for shell commands. Defaults to ["cmd", "/C"] on Windows and ["sh", "-c"] otherwise. - pub shell: Vec, - /// Line number mode. - pub line_number: LineNumber, - /// Highlight the lines cursors are currently on. Defaults to false. - pub cursorline: bool, - /// Highlight the columns cursors are currently on. Defaults to false. - pub cursorcolumn: bool, - #[serde(deserialize_with = "deserialize_gutter_seq_or_struct")] - pub gutters: GutterConfig, - /// Middle click paste support. Defaults to true. - pub middle_click_paste: bool, - /// Automatic insertion of pairs to parentheses, brackets, - /// etc. Optionally, this can be a list of 2-tuples to specify a - /// global list of characters to pair. Defaults to true. - pub auto_pairs: AutoPairConfig, - /// Automatic auto-completion, automatically pop up without user trigger. Defaults to true. - pub auto_completion: bool, - /// Automatic formatting on save. Defaults to true. - pub auto_format: bool, - /// Automatic save on focus lost. Defaults to false. - pub auto_save: bool, - /// Set a global text_width - pub text_width: usize, - /// Time in milliseconds since last keypress before idle timers trigger. - /// Used for autocompletion, set to 0 for instant. Defaults to 250ms. - #[serde( - serialize_with = "serialize_duration_millis", - deserialize_with = "deserialize_duration_millis" - )] - pub idle_timeout: Duration, - /// Whether to insert the completion suggestion on hover. Defaults to true. - pub preview_completion_insert: bool, - pub completion_trigger_len: u8, - /// Whether to instruct the LSP to replace the entire word when applying a completion - /// or to only insert new text - pub completion_replace: bool, - /// Whether to display infoboxes. Defaults to true. - pub auto_info: bool, - pub file_picker: FilePickerConfig, - /// Configuration of the statusline elements - pub statusline: StatusLineConfig, - /// Shape for cursor in each mode - pub cursor_shape: CursorShapeConfig, - /// Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. Defaults to `false`. - pub true_color: bool, - /// Set to `true` to override automatic detection of terminal undercurl support in the event of a false negative. Defaults to `false`. - pub undercurl: bool, - /// Search configuration. - #[serde(default)] - pub search: SearchConfig, - pub lsp: LspConfig, - pub terminal: Option, - /// Column numbers at which to draw the rulers. Defaults to `[]`, meaning no rulers. - pub rulers: Vec, - #[serde(default)] - pub whitespace: WhitespaceConfig, - /// Persistently display open buffers along the top - pub bufferline: BufferLine, - /// Vertical indent width guides. - pub indent_guides: IndentGuidesConfig, - /// Whether to color modes with different colors. Defaults to `false`. - pub color_modes: bool, - pub soft_wrap: SoftWrap, - /// Workspace specific lsp ceiling dirs - pub workspace_lsp_roots: Vec, - /// Which line ending to choose for new documents. Defaults to `native`. i.e. `crlf` on Windows, otherwise `lf`. - pub default_line_ending: LineEndingConfig, - /// Whether to automatically insert a trailing line-ending on write if missing. Defaults to `true`. - pub insert_final_newline: bool, - /// Enables smart tab - pub smart_tab: Option, - /// Draw border around popups. - pub popup_border: PopupBorderConfig, - /// Which indent heuristic to use when a new line is inserted - #[serde(default)] - pub indent_heuristic: IndentationHeuristic, -} - -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)] -#[serde(rename_all = "kebab-case", default)] -pub struct SmartTabConfig { - pub enable: bool, - pub supersede_menu: bool, -} - -impl Default for SmartTabConfig { - fn default() -> Self { - SmartTabConfig { - enable: true, - supersede_menu: false, - } - } -} - -#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(default, rename_all = "kebab-case", deny_unknown_fields)] -pub struct TerminalConfig { - pub command: String, - #[serde(default)] - #[serde(skip_serializing_if = "Vec::is_empty")] - pub args: Vec, -} - -#[cfg(windows)] -pub fn get_terminal_provider() -> Option { - use crate::env::binary_exists; - - if binary_exists("wt") { - return Some(TerminalConfig { - command: "wt".to_string(), - args: vec![ - "new-tab".to_string(), - "--title".to_string(), - "DEBUG".to_string(), - "cmd".to_string(), - "/C".to_string(), - ], - }); - } - - Some(TerminalConfig { - command: "conhost".to_string(), - args: vec!["cmd".to_string(), "/C".to_string()], - }) -} - -#[cfg(not(any(windows, target_os = "wasm32")))] -pub fn get_terminal_provider() -> Option { - use crate::env::{binary_exists, env_var_is_set}; - - if env_var_is_set("TMUX") && binary_exists("tmux") { - return Some(TerminalConfig { - command: "tmux".to_string(), - args: vec!["split-window".to_string()], - }); - } - - if env_var_is_set("WEZTERM_UNIX_SOCKET") && binary_exists("wezterm") { - return Some(TerminalConfig { - command: "wezterm".to_string(), - args: vec!["cli".to_string(), "split-pane".to_string()], - }); - } - - None -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(default, rename_all = "kebab-case", deny_unknown_fields)] -pub struct LspConfig { - /// Enables LSP - pub enable: bool, - /// Display LSP progress messages below statusline - pub display_messages: bool, - /// Enable automatic pop up of signature help (parameter hints) - pub auto_signature_help: bool, - /// Display docs under signature help popup - pub display_signature_help_docs: bool, - /// Display inlay hints - pub display_inlay_hints: bool, - /// Whether to enable snippet support - pub snippets: bool, - /// Whether to include declaration in the goto reference query - pub goto_reference_include_declaration: bool, -} - -impl Default for LspConfig { - fn default() -> Self { - Self { - enable: true, - display_messages: false, - auto_signature_help: true, - display_signature_help_docs: true, - display_inlay_hints: false, - snippets: true, - goto_reference_include_declaration: true, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case", default, deny_unknown_fields)] -pub struct SearchConfig { - /// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true. - pub smart_case: bool, - /// Whether the search should wrap after depleting the matches. Default to true. - pub wrap_around: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case", default, deny_unknown_fields)] -pub struct StatusLineConfig { - pub left: Vec, - pub center: Vec, - pub right: Vec, - pub separator: String, - pub mode: ModeConfig, -} - -impl Default for StatusLineConfig { - fn default() -> Self { - use StatusLineElement as E; - - Self { - left: vec![ - E::Mode, - E::Spinner, - E::FileName, - E::ReadOnlyIndicator, - E::FileModificationIndicator, - ], - center: vec![], - right: vec![ - E::Diagnostics, - E::Selections, - E::Register, - E::Position, - E::FileEncoding, - ], - separator: String::from("│"), - mode: ModeConfig::default(), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case", default, deny_unknown_fields)] -pub struct ModeConfig { - pub normal: String, - pub insert: String, - pub select: String, -} - -impl Default for ModeConfig { - fn default() -> Self { - Self { - normal: String::from("NOR"), - insert: String::from("INS"), - select: String::from("SEL"), - } - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum StatusLineElement { - /// The editor mode (Normal, Insert, Visual/Selection) - Mode, - - /// The LSP activity spinner - Spinner, - - /// The file basename (the leaf of the open file's path) - FileBaseName, - - /// The relative file path - FileName, - - // The file modification indicator - FileModificationIndicator, - - /// An indicator that shows `"[readonly]"` when a file cannot be written - ReadOnlyIndicator, - - /// The file encoding - FileEncoding, - - /// The file line endings (CRLF or LF) - FileLineEnding, - - /// The file type (language ID or "text") - FileType, - - /// A summary of the number of errors and warnings - Diagnostics, - - /// A summary of the number of errors and warnings on file and workspace - WorkspaceDiagnostics, - - /// The number of selections (cursors) - Selections, - - /// The number of characters currently in primary selection - PrimarySelectionLength, - - /// The cursor position - Position, - - /// The separator string - Separator, - - /// The cursor position as a percent of the total file - PositionPercentage, - - /// The total line numbers of the current file - TotalLineNumbers, - - /// A single space - Spacer, - - /// Current version control information - VersionControl, - - /// Indicator for selected register - Register, -} - -// Cursor shape is read and used on every rendered frame and so needs -// to be fast. Therefore we avoid a hashmap and use an enum indexed array. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct CursorShapeConfig([CursorKind; 3]); - -impl CursorShapeConfig { - pub fn from_mode(&self, mode: Mode) -> CursorKind { - self.get(mode as usize).copied().unwrap_or_default() - } -} - -impl<'de> Deserialize<'de> for CursorShapeConfig { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let m = HashMap::::deserialize(deserializer)?; - let into_cursor = |mode: Mode| m.get(&mode).copied().unwrap_or_default(); - Ok(CursorShapeConfig([ - into_cursor(Mode::Normal), - into_cursor(Mode::Select), - into_cursor(Mode::Insert), - ])) - } -} - -impl Serialize for CursorShapeConfig { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let mut map = serializer.serialize_map(Some(self.len()))?; - let modes = [Mode::Normal, Mode::Select, Mode::Insert]; - for mode in modes { - map.serialize_entry(&mode, &self.from_mode(mode))?; - } - map.end() - } -} - -impl std::ops::Deref for CursorShapeConfig { - type Target = [CursorKind; 3]; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Default for CursorShapeConfig { - fn default() -> Self { - Self([CursorKind::Block; 3]) - } -} - -/// bufferline render modes -#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum BufferLine { - /// Don't render bufferline - #[default] - Never, - /// Always render - Always, - /// Only if multiple buffers are open - Multiple, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum LineNumber { - /// Show absolute line number - Absolute, - - /// If focused and in normal/select mode, show relative line number to the primary cursor. - /// If unfocused or in insert mode, show absolute line number. - Relative, -} - -impl std::str::FromStr for LineNumber { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "absolute" | "abs" => Ok(Self::Absolute), - "relative" | "rel" => Ok(Self::Relative), - _ => anyhow::bail!("Line number can only be `absolute` or `relative`."), - } - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum GutterType { - /// Show diagnostics and other features like breakpoints - Diagnostics, - /// Show line numbers - LineNumbers, - /// Show one blank space - Spacer, - /// Highlight local changes - Diff, -} - -impl std::str::FromStr for GutterType { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "diagnostics" => Ok(Self::Diagnostics), - "spacer" => Ok(Self::Spacer), - "line-numbers" => Ok(Self::LineNumbers), - "diff" => Ok(Self::Diff), - _ => anyhow::bail!( - "Gutter type can only be `diagnostics`, `spacer`, `line-numbers` or `diff`." - ), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(default)] -pub struct WhitespaceConfig { - pub render: WhitespaceRender, - pub characters: WhitespaceCharacters, -} - -impl Default for WhitespaceConfig { - fn default() -> Self { - Self { - render: WhitespaceRender::Basic(WhitespaceRenderValue::None), - characters: WhitespaceCharacters::default(), - } - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(untagged, rename_all = "kebab-case")] -pub enum WhitespaceRender { - Basic(WhitespaceRenderValue), - Specific { - default: Option, - space: Option, - nbsp: Option, - tab: Option, - newline: Option, - }, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum WhitespaceRenderValue { - None, - // TODO - // Selection, - All, -} - -impl WhitespaceRender { - pub fn space(&self) -> WhitespaceRenderValue { - match *self { - Self::Basic(val) => val, - Self::Specific { default, space, .. } => { - space.or(default).unwrap_or(WhitespaceRenderValue::None) - } - } - } - pub fn nbsp(&self) -> WhitespaceRenderValue { - match *self { - Self::Basic(val) => val, - Self::Specific { default, nbsp, .. } => { - nbsp.or(default).unwrap_or(WhitespaceRenderValue::None) - } - } - } - pub fn tab(&self) -> WhitespaceRenderValue { - match *self { - Self::Basic(val) => val, - Self::Specific { default, tab, .. } => { - tab.or(default).unwrap_or(WhitespaceRenderValue::None) - } - } - } - pub fn newline(&self) -> WhitespaceRenderValue { - match *self { - Self::Basic(val) => val, - Self::Specific { - default, newline, .. - } => newline.or(default).unwrap_or(WhitespaceRenderValue::None), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(default)] -pub struct WhitespaceCharacters { - pub space: char, - pub nbsp: char, - pub tab: char, - pub tabpad: char, - pub newline: char, -} - -impl Default for WhitespaceCharacters { - fn default() -> Self { - Self { - space: '·', // U+00B7 - nbsp: '⍽', // U+237D - tab: '→', // U+2192 - newline: '⏎', // U+23CE - tabpad: ' ', - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(default, rename_all = "kebab-case")] -pub struct IndentGuidesConfig { - pub render: bool, - pub character: char, - pub skip_levels: u8, -} - -impl Default for IndentGuidesConfig { - fn default() -> Self { - Self { - skip_levels: 0, - render: false, - character: '│', - } - } -} - -/// Line ending configuration. -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum LineEndingConfig { - /// The platform's native line ending. - /// - /// `crlf` on Windows, otherwise `lf`. - #[default] - Native, - /// Line feed. - LF, - /// Carriage return followed by line feed. - Crlf, - /// Form feed. - #[cfg(feature = "unicode-lines")] - FF, - /// Carriage return. - #[cfg(feature = "unicode-lines")] - CR, - /// Next line. - #[cfg(feature = "unicode-lines")] - Nel, -} - -impl From for LineEnding { - fn from(line_ending: LineEndingConfig) -> Self { - match line_ending { - LineEndingConfig::Native => NATIVE_LINE_ENDING, - LineEndingConfig::LF => LineEnding::LF, - LineEndingConfig::Crlf => LineEnding::Crlf, - #[cfg(feature = "unicode-lines")] - LineEndingConfig::FF => LineEnding::FF, - #[cfg(feature = "unicode-lines")] - LineEndingConfig::CR => LineEnding::CR, - #[cfg(feature = "unicode-lines")] - LineEndingConfig::Nel => LineEnding::Nel, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum PopupBorderConfig { - None, - All, - Popup, - Menu, -} - -impl Default for Config { - fn default() -> Self { - Self { - scrolloff: 5, - scroll_lines: 3, - mouse: true, - shell: if cfg!(windows) { - vec!["cmd".to_owned(), "/C".to_owned()] - } else { - vec!["sh".to_owned(), "-c".to_owned()] - }, - line_number: LineNumber::Absolute, - cursorline: false, - cursorcolumn: false, - gutters: GutterConfig::default(), - middle_click_paste: true, - auto_pairs: AutoPairConfig::default(), - auto_completion: true, - auto_format: true, - auto_save: false, - idle_timeout: Duration::from_millis(250), - preview_completion_insert: true, - completion_trigger_len: 2, - auto_info: true, - file_picker: FilePickerConfig::default(), - statusline: StatusLineConfig::default(), - cursor_shape: CursorShapeConfig::default(), - true_color: false, - undercurl: false, - search: SearchConfig::default(), - lsp: LspConfig::default(), - terminal: get_terminal_provider(), - rulers: Vec::new(), - whitespace: WhitespaceConfig::default(), - bufferline: BufferLine::default(), - indent_guides: IndentGuidesConfig::default(), - color_modes: false, - soft_wrap: SoftWrap { - enable: Some(false), - ..SoftWrap::default() - }, - text_width: 80, - completion_replace: false, - workspace_lsp_roots: Vec::new(), - default_line_ending: LineEndingConfig::default(), - insert_final_newline: true, - smart_tab: Some(SmartTabConfig::default()), - popup_border: PopupBorderConfig::None, - indent_heuristic: IndentationHeuristic::default(), - } - } -} - -impl Default for SearchConfig { - fn default() -> Self { - Self { - wrap_around: true, - smart_case: true, - } - } -} +use arc_swap::access::DynGuard; #[derive(Debug, Clone, Default)] pub struct Breakpoint {