Unnamed repository; edit this file 'description' to name the repository.
-rw-r--r--Cargo.lock6
-rw-r--r--helix-config/Cargo.toml4
-rw-r--r--helix-config/src/definition.rs113
-rw-r--r--helix-config/src/definition/language.rs27
-rw-r--r--helix-config/src/definition/lsp.rs266
-rw-r--r--helix-config/src/definition/ui.rs291
-rw-r--r--helix-config/src/env.rs10
-rw-r--r--helix-config/src/lib.rs4
-rw-r--r--helix-core/src/auto_pairs.rs58
-rw-r--r--helix-core/src/indent.rs66
-rw-r--r--helix-core/src/lib.rs7
-rw-r--r--helix-core/src/line_ending.rs58
-rw-r--r--helix-dap/Cargo.toml1
-rw-r--r--helix-dap/src/config.rs146
-rw-r--r--helix-dap/src/lib.rs1
-rw-r--r--helix-view/Cargo.toml1
-rw-r--r--helix-view/src/gutter.rs85
17 files changed, 1133 insertions, 11 deletions
diff --git a/Cargo.lock b/Cargo.lock
index e97daba3..22ea5640 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1053,11 +1053,15 @@ version = "23.10.0"
dependencies = [
"ahash",
"anyhow",
+ "globset",
"hashbrown 0.14.3",
"indexmap",
"parking_lot",
+ "regex",
+ "regex-syntax",
"serde",
"serde_json",
+ "which",
]
[[package]]
@@ -1102,6 +1106,7 @@ version = "23.10.0"
dependencies = [
"anyhow",
"fern",
+ "helix-config",
"helix-core",
"log",
"serde",
@@ -1250,6 +1255,7 @@ dependencies = [
"clipboard-win",
"crossterm",
"futures-util",
+ "helix-config",
"helix-core",
"helix-dap",
"helix-event",
diff --git a/helix-config/Cargo.toml b/helix-config/Cargo.toml
index ba9bbb5d..86bcceaf 100644
--- a/helix-config/Cargo.toml
+++ b/helix-config/Cargo.toml
@@ -19,6 +19,10 @@ anyhow = "1.0.79"
indexmap = { version = "2.1.0", features = ["serde"] }
serde = { version = "1.0" }
serde_json = "1.0"
+globset = "0.4.14"
+regex = "1.10.2"
+regex-syntax = "0.8.2"
+which = "5.0.0"
regex-syntax = "0.8.2"
which = "5.0.0"
diff --git a/helix-config/src/definition.rs b/helix-config/src/definition.rs
new file mode 100644
index 00000000..b51f2d75
--- /dev/null
+++ b/helix-config/src/definition.rs
@@ -0,0 +1,113 @@
+use std::time::Duration;
+
+use crate::*;
+
+mod language;
+mod lsp;
+mod ui;
+
+pub use lsp::init_language_server_config;
+
+options! {
+ use ui::*;
+ use lsp::*;
+ use language::*;
+
+ struct WrapConfig {
+ /// Soft wrap lines that exceed viewport width.
+ enable: bool = false,
+ /// Maximum free space left at the end of the line.
+ /// Automatically limited to a quarter of the viewport.
+ max_wrap: u16 = 20,
+ /// Maximum indentation to carry over when soft wrapping a line.
+ /// Automatically limited to a quarter of the viewport.
+ max_indent_retain: u16 = 40,
+ /// Text inserted before soft wrapped lines, highlighted with `ui.virtual.wrap`.
+ wrap_indicator: String = "↪",
+ /// Soft wrap at `text-width` instead of using the full viewport size.
+ wrap_at_text_width: bool = false,
+ /// Maximum line length. Used for the `:reflow` command and
+ /// soft-wrapping if `soft-wrap.wrap-at-text-width` is set
+ text_width: usize = 80,
+ }
+
+ struct MouseConfig {
+ /// Enable mouse mode
+ #[read = copy]
+ mouse: bool = true,
+ /// Number of lines to scroll per scroll wheel step.
+ #[read = copy]
+ scroll_lines: usize = 3,
+ /// Middle click paste support
+ #[read = copy]
+ middle_click_paste: bool = true,
+ }
+ struct SmartTabConfig {
+ /// If set to true, then when the cursor is in a position with
+ /// non-whitespace to its left, instead of inserting a tab, it will run
+ /// `move_parent_node_end`. If there is only whitespace to the left,
+ /// then it inserts a tab as normal. With the default bindings, to
+ /// explicitly insert a tab character, press Shift-tab.
+ #[name = "smart-tab.enable"]
+ #[read = copy]
+ enable: bool = true,
+ /// Normally, when a menu is on screen, such as when auto complete
+ /// is triggered, the tab key is bound to cycling through the items.
+ /// This means when menus are on screen, one cannot use the tab key
+ /// to trigger the `smart-tab` command. If this option is set to true,
+ /// the `smart-tab` command always takes precedence, which means one
+ /// cannot use the tab key to cycle through menu items. One of the other
+ /// bindings must be used instead, such as arrow keys or `C-n`/`C-p`.
+ #[name = "smart-tab.supersede-menu"]
+ #[read = copy]
+ supersede_menu: bool = false,
+ }
+
+ struct SearchConfig {
+ /// Enable smart case regex searching (case-insensitive unless pattern
+ /// contains upper case characters)
+ #[name = "search.smart-case"]
+ #[read = copy]
+ smart_case: bool = true,
+ /// Whether the search should wrap after depleting the matches
+ #[name = "search.wrap-round"]
+ #[read = copy]
+ wrap_round: bool = true,
+ }
+
+ struct MiscConfig {
+ /// Number of lines of padding around the edge of the screen when scrolling.
+ #[read = copy]
+ scrolloff: usize = 5,
+ /// Shell to use when running external commands
+ #[read = deref]
+ shell: List<String> = if cfg!(windows) {
+ &["cmd", "/C"]
+ } else {
+ &["sh", "-c"]
+ },
+ /// Enable automatic saving on the focus moving away from Helix.
+ /// Requires [focus event support](https://github.com/helix-editor/
+ /// helix/wiki/Terminal-Support) from your terminal
+ #[read = copy]
+ auto_save: bool = false,
+ /// Whether to automatically insert a trailing line-ending on write
+ /// if missing
+ #[read = copy]
+ insert_final_newline: bool = true,
+ /// Time in milliseconds since last keypress before idle timers trigger.
+ /// Used for autocompletion, set to 0 for instant
+ #[read = copy]
+ idle_timeout: Duration = Duration::from_millis(250),
+ }
+}
+
+impl Ty for Duration {
+ fn from_value(val: Value) -> anyhow::Result<Self> {
+ let val: usize = val.typed()?;
+ Ok(Duration::from_millis(val as _))
+ }
+ fn to_value(&self) -> Value {
+ Value::Int(self.as_millis().try_into().unwrap())
+ }
+}
diff --git a/helix-config/src/definition/language.rs b/helix-config/src/definition/language.rs
new file mode 100644
index 00000000..5042823a
--- /dev/null
+++ b/helix-config/src/definition/language.rs
@@ -0,0 +1,27 @@
+use crate::*;
+
+options! {
+ struct LanguageConfig {
+ /// regex pattern that will be tested against a language name in order to determine whether this language should be used for a potential [language injection][treesitter-language-injection] site.
+ #[validator = regex_str_validator()]
+ injection_regex: Option<String> = None,
+ /// The interpreters from the shebang line, for example `["sh", "bash"]`
+ #[read = deref]
+ shebangs: List<String> = List::default(),
+ /// The token to use as a comment-token
+ #[read = deref]
+ comment_token: String = "//",
+ /// The tree-sitter grammar to use (defaults to the language name)
+ grammar: Option<String> = None,
+ }
+
+ struct FormatterConfiguration {
+ #[read = copy]
+ auto_format: bool = true,
+ #[name = "formatter.command"]
+ formatter_command: Option<String> = None,
+ #[name = "formatter.args"]
+ #[read = deref]
+ formatter_args: List<String> = List::default(),
+ }
+}
diff --git a/helix-config/src/definition/lsp.rs b/helix-config/src/definition/lsp.rs
new file mode 100644
index 00000000..2c6845f4
--- /dev/null
+++ b/helix-config/src/definition/lsp.rs
@@ -0,0 +1,266 @@
+use std::fmt::{self, Display};
+
+use serde::{Deserialize, Serialize};
+
+use crate::*;
+
+/// Describes the severity level of a [`Diagnostic`].
+#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Deserialize, Serialize)]
+pub enum Severity {
+ Hint,
+ Info,
+ Warning,
+ Error,
+}
+
+impl Ty for Severity {
+ fn from_value(val: Value) -> anyhow::Result<Self> {
+ let val: String = val.typed()?;
+ match &*val {
+ "hint" => Ok(Severity::Hint),
+ "info" => Ok(Severity::Info),
+ "warning" => Ok(Severity::Warning),
+ "error" => Ok(Severity::Error),
+ _ => bail!("expected one of 'hint', 'info', 'warning' or 'error' (got {val:?})"),
+ }
+ }
+
+ fn to_value(&self) -> Value {
+ match self {
+ Severity::Hint => "hint".into(),
+ Severity::Info => "info".into(),
+ Severity::Warning => "warning".into(),
+ Severity::Error => "error".into(),
+ }
+ }
+}
+
+// TODO: move to stdx
+/// Helper macro that automatically generates an array
+/// that contains all variants of an enum
+macro_rules! variant_list {
+ (
+ $(#[$outer:meta])*
+ $vis: vis enum $name: ident {
+ $($(#[$inner: meta])* $variant: ident $(= $_: literal)?),*$(,)?
+ }
+ ) => {
+ $(#[$outer])*
+ $vis enum $name {
+ $($(#[$inner])* $variant),*
+ }
+ impl $name {
+ $vis const ALL: &[$name] = &[$(Self::$variant),*];
+ }
+ }
+}
+variant_list! {
+ #[derive(Clone, Copy, PartialEq, Eq, Hash)]
+ 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 LanguageServerFeature {
+ fn to_str(self) -> &'static str {
+ use LanguageServerFeature::*;
+
+ match self {
+ Format => "format",
+ GotoDeclaration => "goto-declaration",
+ GotoDefinition => "goto-definition",
+ GotoTypeDefinition => "goto-type-definition",
+ GotoReference => "goto-reference",
+ 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",
+ }
+ }
+ fn description(self) -> &'static str {
+ use LanguageServerFeature::*;
+
+ match self {
+ Format => "Use this language server for autoformatting.",
+ GotoDeclaration => "Use this language server for the goto_declaration command.",
+ GotoDefinition => "Use this language server for the goto_definition command.",
+ GotoTypeDefinition => "Use this language server for the goto_type_definition command.",
+ GotoReference => "Use this language server for the goto_reference command.",
+ GotoImplementation => "Use this language server for the goto_implementation command.",
+ SignatureHelp => "Use this language server to display signature help.",
+ Hover => "Use this language server to display hover information.",
+ DocumentHighlight => {
+ "Use this language server for the select_references_to_symbol_under_cursor command."
+ }
+ Completion => "Request completion items from this language server.",
+ CodeAction => "Use this language server for the code_action command.",
+ WorkspaceCommand => "Use this language server for :lsp-workspace-command.",
+ DocumentSymbols => "Use this language server for the symbol_picker command.",
+ WorkspaceSymbols => "Use this language server for the workspace_symbol_picker command.",
+ Diagnostics => "Display diagnostics emitted by this language server.",
+ RenameSymbol => "Use this language server for the rename_symbol command.",
+ InlayHints => "Display inlay hints form this language server.",
+ }
+ }
+}
+
+impl Display for LanguageServerFeature {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let feature = self.to_str();
+ write!(f, "{feature}",)
+ }
+}
+
+impl Debug for LanguageServerFeature {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{self}")
+ }
+}
+
+impl Ty for LanguageServerFeature {
+ fn from_value(val: Value) -> anyhow::Result<Self> {
+ let val: String = val.typed()?;
+ use LanguageServerFeature::*;
+
+ match &*val {
+ "format" => Ok(Format),
+ "goto-declaration" => Ok(GotoDeclaration),
+ "goto-definition" => Ok(GotoDefinition),
+ "goto-type-definition" => Ok(GotoTypeDefinition),
+ "goto-reference" => Ok(GotoReference),
+ "goto-implementation" => Ok(GotoImplementation),
+ "signature-help" => Ok(SignatureHelp),
+ "hover" => Ok(Hover),
+ "document-highlight" => Ok(DocumentHighlight),
+ "completion" => Ok(Completion),
+ "code-action" => Ok(CodeAction),
+ "workspace-command" => Ok(WorkspaceCommand),
+ "document-symbols" => Ok(DocumentSymbols),
+ "workspace-symbols" => Ok(WorkspaceSymbols),
+ "diagnostics" => Ok(Diagnostics),
+ "rename-symbol" => Ok(RenameSymbol),
+ "inlay-hints" => Ok(InlayHints),
+ _ => bail!("invalid language server feature {val}"),
+ }
+ }
+
+ fn to_value(&self) -> Value {
+ Value::String(self.to_str().into())
+ }
+}
+
+pub fn init_language_server_config(registry: &mut OptionRegistry, languag_server: &str) {
+ registry.register(
+ &format!("language-servers.{languag_server}.active"),
+ "Wether this language servers is used for a buffer",
+ false,
+ );
+ for &feature in LanguageServerFeature::ALL {
+ registry.register(
+ &format!("language-servers.{languag_server}.{feature}"),
+ feature.description(),
+ true,
+ );
+ }
+}
+
+options! {
+ struct LspConfig {
+ /// Enables LSP integration. Setting to false will completely disable language servers.
+ #[name = "lsp.enable"]
+ #[read = copy]
+ enable: bool = true,
+ /// Enables LSP integration. Setting to false will completely disable language servers.
+ #[name = "lsp.display-messages"]
+ #[read = copy]
+ display_messages: bool = false,
+ /// Enable automatic popup of signature help (parameter hints)
+ #[name = "lsp.auto-signature-help"]
+ #[read = copy]
+ auto_signature_help: bool = true,
+ /// Enable automatic popup of signature help (parameter hints)
+ #[name = "lsp.display-inlay-hints"]
+ #[read = copy]
+ display_inlay_hints: bool = false,
+ /// Display docs under signature help popup
+ #[name = "lsp.display-signature-help-docs"]
+ #[read = copy]
+ display_signature_help_docs: bool = true,
+ /// Enables snippet completions. Requires a server restart
+ /// (`:lsp-restart`) to take effect after `:config-reload`/`:set`.
+ #[name = "lsp.snippets"]
+ #[read = copy]
+ snippets: bool = true,
+ /// Include declaration in the goto references popup.
+ #[name = "lsp.goto-reference-include-declaration"]
+ #[read = copy]
+ goto_reference_include_declaration: bool = true,
+ // TODO(breaing): prefix all options below with `lsp.`
+ /// The language-id for language servers, checkout the
+ /// table at [TextDocumentItem](https://microsoft.github.io/
+ /// language-server-protocol/specifications/lsp/3.17/specification/
+ /// #textDocumentItem) for the right id
+ #[name = "languague-id"]
+ language_server_id: Option<String> = None,
+ // TODO(breaking): rename to root-markers to differentiate from workspace-roots
+ // TODO: also makes this setteble on the language server
+ /// A set of marker files to look for when trying to find the workspace
+ /// root. For example `Cargo.lock`, `yarn.lock`
+ roots: List<String> = List::default(),
+ // TODO: also makes this setteble on the language server
+ /// Directories relative to the workspace root that are treated as LSP
+ /// roots. The search for root markers (starting at the path of the
+ /// file) will stop at these paths.
+ #[name = "workspace-lsp-roots"]
+ workspace_roots: List<String> = List::default(),
+ /// An array of LSP diagnostic sources assumed unchanged when the
+ /// language server resends the same set of diagnostics. Helix can track
+ /// the position for these diagnostics internally instead. Useful for
+ /// diagnostics that are recomputed on save.
+ persistent_diagnostic_sources: List<String> = List::default(),
+ /// Minimal severity of diagnostic for it to be displayed. (Allowed
+ /// values: `error`, `warning`, `info`, `hint`)
+ diagnostic_severity: Severity = Severity::Hint,
+ }
+
+ struct CompletionConfig {
+ /// Automatic auto-completion, automatically pop up without user trigger.
+ #[read = copy]
+ auto_completion: bool = true,
+ /// Whether to apply completion item instantly when selected
+ #[read = copy]
+ preview_completion_insert: bool = true,
+ /// Whether to apply completion item instantly when selected
+ #[read = copy]
+ completion_replace: bool = false,
+ /// Whether to apply completion item instantly when selected
+ #[read = copy]
+ completion_trigger_len: u8 = 2,
+ }
+}
diff --git a/helix-config/src/definition/ui.rs b/helix-config/src/definition/ui.rs
new file mode 100644
index 00000000..378d4e05
--- /dev/null
+++ b/helix-config/src/definition/ui.rs
@@ -0,0 +1,291 @@
+use serde::{Deserialize, Serialize};
+
+use crate::*;
+
+#[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,
+}
+
+config_serde_adapter!(StatusLineElement);
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+/// UNSTABLE
+pub enum CursorKind {
+ /// █
+ Block,
+ /// |
+ Bar,
+ /// _
+ Underline,
+ /// Hidden cursor, can set cursor position with this to let IME have correct cursor position.
+ Hidden,
+}
+
+impl Default for CursorKind {
+ fn default() -> Self {
+ Self::Block
+ }
+}
+
+config_serde_adapter!(CursorKind);
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum WhitespaceRenderValue {
+ None,
+ // TODO
+ // Selection,
+ All,
+}
+
+config_serde_adapter!(WhitespaceRenderValue);
+
+/// bufferline render modes
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum BufferLine {
+ /// Don't render bufferline
+ Never,
+ /// Always render
+ Always,
+ /// Only if multiple buffers are open
+ Multiple,
+}
+
+config_serde_adapter!(BufferLine);
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum PopupBorderConfig {
+ None,
+ All,
+ Popup,
+ Menu,
+}
+
+config_serde_adapter!(PopupBorderConfig);
+
+options! {
+ struct UiConfig {
+ /// Whether to display info boxes
+ #[read = copy]
+ auto_info: bool = true,
+ /// Renders a line at the top of the editor displaying open buffers.
+ /// Can be `always`, `never` or `multiple` (only shown if more than one
+ /// buffer is in use)
+ #[read = copy]
+ bufferline: BufferLine = BufferLine::Never,
+ /// Highlight all lines with a cursor
+ #[read = copy]
+ cursorline: bool = false,
+ /// Highlight all columns with a cursor
+ #[read = copy]
+ cursorcolumn: bool = false,
+ /// List of column positions at which to display the rulers.
+ #[read = deref]
+ rulers: List<u16> = List::default(),
+ /// Whether to color the mode indicator with different colors depending on the mode itself
+ #[read = copy]
+ popup_border: bool = false,
+ /// Whether to color the mode indicator with different colors depending on the mode itself
+ #[read = copy]
+ color_modes: bool = false,
+ }
+
+ struct WhiteSpaceRenderConfig {
+ #[name = "whitespace.characters.space"]
+ #[read = copy]
+ space_char: char = '·', // U+00B7
+ #[name = "whitespace.characters.nbsp"]
+ #[read = copy]
+ nbsp_char: char = '⍽', // U+237D
+ #[name = "whitespace.characters.tab"]
+ #[read = copy]
+ tab_char: char = '→', // U+2192
+ #[name = "whitespace.characters.tabpad"]
+ #[read = copy]
+ tabpad_char: char = '⏎', // U+23CE
+ #[name = "whitespace.characters.newline"]
+ #[read = copy]
+ newline_char: char = ' ',
+ #[name = "whitespace.render.default"]
+ #[read = copy]
+ render: WhitespaceRenderValue = WhitespaceRenderValue::None,
+ #[name = "whitespace.render.space"]
+ #[read = copy]
+ render_space: Option<WhitespaceRenderValue> = None,
+ #[name = "whitespace.render.nbsp"]
+ #[read = copy]
+ render_nbsp: Option<WhitespaceRenderValue> = None,
+ #[name = "whitespace.render.tab"]
+ #[read = copy]
+ render_tab: Option<WhitespaceRenderValue> = None,
+ #[name = "whitespace.render.newline"]
+ #[read = copy]
+ render_newline: Option<WhitespaceRenderValue> = None,
+ }
+
+ struct TerminfoConfig {
+ /// Set to `true` to override automatic detection of terminal truecolor
+ /// support in the event of a false negative
+ #[name = "true-color"]
+ #[read = copy]
+ force_true_color: bool = false,
+ /// Set to `true` to override automatic detection of terminal undercurl
+ /// support in the event of a false negative
+ #[name = "undercurl"]
+ #[read = copy]
+ force_undercurl: bool = false,
+ }
+
+ struct IndentGuidesConfig {
+ /// Whether to render indent guides
+ #[read = copy]
+ render: bool = false,
+ /// Character to use for rendering indent guides
+ #[read = copy]
+ character: char = '│',
+ /// Number of indent levels to skip
+ #[read = copy]
+ skip_levels: u8 = 0,
+ }
+
+ struct CursorShapeConfig {
+ /// Cursor shape in normal mode
+ #[name = "cursor-shape.normal"]
+ #[read = copy]
+ normal_mode_cursor: CursorKind = CursorKind::Block,
+ /// Cursor shape in select mode
+ #[name = "cursor-shape.select"]
+ #[read = copy]
+ select_mode_cursor: CursorKind = CursorKind::Block,
+ /// Cursor shape in insert mode
+ #[name = "cursor-shape.insert"]
+ #[read = copy]
+ insert_mode_cursor: CursorKind = CursorKind::Block,
+ }
+
+ struct FilePickerConfig {
+ /// Whether to exclude hidden files from any file pickers.
+ #[name = "file-picker.hidden"]
+ #[read = copy]
+ hidden: bool = true,
+ /// Follow symlinks instead of ignoring them
+ #[name = "file-picker.follow-symlinks"]
+ #[read = copy]
+ follow_symlinks: bool = true,
+ /// Ignore symlinks that point at files already shown in the picker
+ #[name = "file-picker.deduplicate-links"]
+ #[read = copy]
+ deduplicate_links: bool = true,
+ /// Enables reading ignore files from parent directories.
+ #[name = "file-picker.parents"]
+ #[read = copy]
+ parents: bool = true,
+ /// Enables reading `.ignore` files.
+ #[name = "file-picker.ignore"]
+ #[read = copy]
+ ignore: bool = true,
+ /// Enables reading `.gitignore` files.
+ #[name = "file-picker.git-ignore"]
+ #[read = copy]
+ git_ignore: bool = true,
+ /// Enables reading global .gitignore, whose path is specified in git's config: `core.excludefile` option.
+ #[name = "file-picker.git-global"]
+ #[read = copy]
+ git_global: bool = true,
+ /// Enables reading `.git/info/exclude` files.
+ #[name = "file-picker.git-exclude"]
+ #[read = copy]
+ git_exclude: bool = true,
+ /// Maximum Depth to recurse directories in file picker and global search.
+ #[name = "file-picker.max-depth"]
+ #[read = copy]
+ max_depth: Option<usize> = None,
+ }
+
+ struct StatusLineConfig{
+ /// A list of elements aligned to the left of the statusline
+ #[name = "statusline.left"]
+ #[read = deref]
+ left: List<StatusLineElement> = &[
+ StatusLineElement::Mode,
+ StatusLineElement::Spinner,
+ StatusLineElement::FileName,
+ StatusLineElement::ReadOnlyIndicator,
+ StatusLineElement::FileModificationIndicator,
+ ],
+ /// A list of elements aligned to the middle of the statusline
+ #[name = "statusline.center"]
+ #[read = deref]
+ center: List<StatusLineElement> = List::default(),
+ /// A list of elements aligned to the right of the statusline
+ #[name = "statusline.right"]
+ #[read = deref]
+ right: List<StatusLineElement> = &[
+ StatusLineElement::Diagnostics,
+ StatusLineElement::Selections,
+ StatusLineElement::Register,
+ StatusLineElement::Position,
+ StatusLineElement::FileEncoding,
+ ],
+ /// The character used to separate elements in the statusline
+ #[name = "statusline.seperator"]
+ #[read = deref]
+ seperator: String = "│",
+ /// The text shown in the `mode` element for normal mode
+ #[name = "statusline.mode.normal"]
+ #[read = deref]
+ mode_indicator_normal: String = "NOR",
+ /// The text shown in the `mode` element for insert mode
+ #[name = "statusline.mode.insert"]
+ #[read = deref]
+ mode_indicator_insert: String = "INS",
+ /// The text shown in the `mode` element for select mode
+ #[name = "statusline.mode.select"]
+ #[read = deref]
+ mode_indicator_select: String = "SEL",
+ }
+}
diff --git a/helix-config/src/env.rs b/helix-config/src/env.rs
new file mode 100644
index 00000000..537db384
--- /dev/null
+++ b/helix-config/src/env.rs
@@ -0,0 +1,10 @@
+// TOOD: move to stdx
+
+pub fn binary_exists(binary_name: &str) -> bool {
+ which::which(binary_name).is_ok()
+}
+
+#[cfg(not(windows))]
+pub fn env_var_is_set(env_var_name: &str) -> bool {
+ std::env::var_os(env_var_name).is_some()
+}
diff --git a/helix-config/src/lib.rs b/helix-config/src/lib.rs
index 8f27e41e..336a57f1 100644
--- a/helix-config/src/lib.rs
+++ b/helix-config/src/lib.rs
@@ -13,13 +13,15 @@ use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard};
use any::ConfigData;
use convert::ty_into_value;
pub use convert::IntoTy;
-pub use definition::init_config;
+pub use definition::{init_config, init_language_server_config};
use validator::StaticValidator;
pub use validator::{regex_str_validator, ty_validator, IntegerRangeValidator, Ty, Validator};
pub use value::{from_value, to_value, Value};
mod any;
mod convert;
+mod definition;
+pub mod env;
mod macros;
mod validator;
mod value;
diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs
index 31f9d364..b8223893 100644
--- a/helix-core/src/auto_pairs.rs
+++ b/helix-core/src/auto_pairs.rs
@@ -2,8 +2,10 @@
//! this module provides the functionality to insert the paired closing character.
use crate::{graphemes, movement::Direction, Range, Rope, Selection, Tendril, Transaction};
-use std::collections::HashMap;
+use anyhow::{bail, ensure};
+use helix_config::options;
+use indexmap::IndexMap;
use smallvec::SmallVec;
// Heavily based on https://github.com/codemirror/closebrackets/
@@ -19,7 +21,7 @@ pub const DEFAULT_PAIRS: &[(char, char)] = &[
/// The type that represents the collection of auto pairs,
/// keyed by both opener and closer.
#[derive(Debug, Clone)]
-pub struct AutoPairs(HashMap<char, Pair>);
+pub struct AutoPairs(IndexMap<char, Pair, ahash::RandomState>);
/// Represents the config for a particular pairing.
#[derive(Debug, Clone, Copy)]
@@ -75,15 +77,15 @@ impl From<(&char, &char)> for Pair {
impl AutoPairs {
/// Make a new AutoPairs set with the given pairs and default conditions.
- pub fn new<'a, V: 'a, A>(pairs: V) -> Self
+ pub fn new<'a, V: 'a, A>(pairs: V) -> anyhow::Result<Self>
where
- V: IntoIterator<Item = A>,
+ V: IntoIterator<Item = anyhow::Result<A>>,
A: Into<Pair>,
{
- let mut auto_pairs = HashMap::new();
+ let mut auto_pairs = IndexMap::default();
for pair in pairs.into_iter() {
- let auto_pair = pair.into();
+ let auto_pair = pair?.into();
auto_pairs.insert(auto_pair.open, auto_pair);
@@ -92,7 +94,7 @@ impl AutoPairs {
}
}
- Self(auto_pairs)
+ Ok(Self(auto_pairs))
}
pub fn get(&self, ch: char) -> Option<&Pair> {
@@ -102,7 +104,7 @@ impl AutoPairs {
impl Default for AutoPairs {
fn default() -> Self {
- AutoPairs::new(DEFAULT_PAIRS.iter())
+ AutoPairs::new(DEFAULT_PAIRS.iter().map(Ok)).unwrap()
}
}
@@ -371,3 +373,43 @@ fn handle_same(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
log::debug!("auto pair transaction: {:#?}", t);
t
}
+
+options! {
+ struct AutopairConfig {
+ /// Mapping of character pairs like `{ '(' = ')', '`' = '`' }` that are
+ /// automatically closed by the editor when typed.
+ auto_pairs: AutoPairs = AutoPairs::default(),
+ }
+}
+
+impl helix_config::Ty for AutoPairs {
+ fn from_value(val: helix_config::Value) -> anyhow::Result<Self> {
+ let map = match val {
+ helix_config::Value::Map(map) => map,
+ helix_config::Value::Bool(false) => return Ok(Self(IndexMap::default())),
+ _ => bail!("expect 'false' or a map of pairs"),
+ };
+ let pairs = map.into_iter().map(|(open, close)| {
+ let open = helix_config::Value::String(open.into_string());
+ Ok(Pair {
+ open: open.typed()?,
+ close: close.typed()?,
+ })
+ });
+ AutoPairs::new(pairs)
+ }
+
+ fn to_value(&self) -> helix_config::Value {
+ let map = self
+ .0
+ .values()
+ .map(|pair| {
+ (
+ pair.open.to_string().into(),
+ helix_config::Value::String(pair.close.into()),
+ )
+ })
+ .collect();
+ helix_config::Value::Map(Box::new(map))
+ }
+}
diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs
index 1e90db47..2a5f5d93 100644
--- a/helix-core/src/indent.rs
+++ b/helix-core/src/indent.rs
@@ -1,16 +1,36 @@
use std::{borrow::Cow, collections::HashMap};
+use anyhow::{anyhow, bail};
+use helix_config::{config_serde_adapter, options, IntegerRangeValidator};
+use serde::{Deserialize, Serialize};
use tree_sitter::{Query, QueryCursor, QueryPredicateArg};
use crate::{
chars::{char_is_line_ending, char_is_whitespace},
find_first_non_whitespace_char,
graphemes::{grapheme_width, tab_width_at},
- syntax::{IndentationHeuristic, LanguageConfiguration, RopeProvider, Syntax},
+ syntax::{LanguageConfiguration, RopeProvider, Syntax},
tree_sitter::Node,
Position, Rope, RopeGraphemes, RopeSlice,
};
+/// 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, Copy, 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,
+}
+config_serde_adapter!(IndentationHeuristic);
+
/// Enum representing indentation style.
///
/// Only values 1-8 are valid for the `Spaces` variant.
@@ -20,6 +40,50 @@ pub enum IndentStyle {
Spaces(u8),
}
+options! {
+ struct IndentationConfig {
+ /// The number columns that a tabs are aligned to.
+ #[name = "ident.tab_width"]
+ #[read = copy]
+ tab_width: usize = 4,
+ /// Indentation inserted/removed into the document when indenting/dedenting.
+ /// This can be set to an integer representing N spaces or "tab" for tabs.
+ #[name = "ident.unit"]
+ #[read = copy]
+ indent_style: IndentStyle = IndentStyle::Tabs,
+ /// How the indentation for a newly inserted line is computed:
+ /// `simple` just copies the indentation level from the previous line,
+ /// `tree-sitter` computes the indentation based on the syntax tree and
+ /// `hybrid` combines both approaches.
+ /// If the chosen heuristic is not available, a different one will
+ /// be used as a fallback (the fallback order being `hybrid` ->
+ /// `tree-sitter` -> `simple`).
+ #[read = copy]
+ indent_heuristic: IndentationHeuristic = IndentationHeuristic::Hybrid
+ }
+}
+
+impl helix_config::Ty for IndentStyle {
+ fn from_value(val: helix_config::Value) -> anyhow::Result<Self> {
+ match val {
+ helix_config::Value::String(s) if s == "t" || s == "tab" => Ok(IndentStyle::Tabs),
+ helix_config::Value::Int(_) => {
+ let spaces = IntegerRangeValidator::new(0, MAX_INDENT)
+ .validate(val)
+ .map_err(|err| anyhow!("invalid number of spaces! {err}"))?;
+ Ok(IndentStyle::Spaces(spaces))
+ }
+ _ => bail!("expected an integer (spaces) or 'tab'"),
+ }
+ }
+ fn to_value(&self) -> helix_config::Value {
+ match *self {
+ IndentStyle::Tabs => helix_config::Value::String("tab".into()),
+ IndentStyle::Spaces(spaces) => helix_config::Value::Int(spaces as _),
+ }
+ }
+}
+
// 16 spaces
const INDENTS: &str = " ";
pub const MAX_INDENT: u8 = 16;
diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs
index b93ee800..8b2edf58 100644
--- a/helix-core/src/lib.rs
+++ b/helix-core/src/lib.rs
@@ -35,6 +35,7 @@ pub mod unicode {
pub use unicode_width as width;
}
+use helix_config::OptionRegistry;
pub use helix_loader::find_workspace;
pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
@@ -69,3 +70,9 @@ pub use diagnostic::Diagnostic;
pub use line_ending::{LineEnding, NATIVE_LINE_ENDING};
pub use transaction::{Assoc, Change, ChangeSet, Deletion, Operation, Transaction};
+
+pub fn init_config(registry: &mut OptionRegistry) {
+ line_ending::init_config(registry);
+ auto_pairs::init_config(registry);
+ indent::init_config(registry);
+}
diff --git a/helix-core/src/line_ending.rs b/helix-core/src/line_ending.rs
index 36c02a94..aa1e7480 100644
--- a/helix-core/src/line_ending.rs
+++ b/helix-core/src/line_ending.rs
@@ -1,3 +1,6 @@
+use anyhow::bail;
+use helix_config::{options, Ty};
+
use crate::{Rope, RopeSlice};
#[cfg(target_os = "windows")]
@@ -5,6 +8,61 @@ pub const NATIVE_LINE_ENDING: LineEnding = LineEnding::Crlf;
#[cfg(not(target_os = "windows"))]
pub const NATIVE_LINE_ENDING: LineEnding = LineEnding::LF;
+options! {
+ struct LineEndingConfig {
+ /// The line ending to use for new documents. Can be `lf` or `crlf`. If
+ /// helix was compiled with the `unicode-lines` feature then `vt`, `ff`,
+ /// `cr`, `nel`, `ls` or `ps` are also allowed.
+ #[read = copy]
+ default_line_ending: LineEnding = NATIVE_LINE_ENDING,
+ }
+}
+
+impl Ty for LineEnding {
+ fn from_value(val: helix_config::Value) -> anyhow::Result<Self> {
+ let val: String = val.typed()?;
+ match &*val {
+ "crlf" => Ok(LineEnding::Crlf),
+ "lf" => Ok(LineEnding::LF),
+ #[cfg(feature = "unicode-lines")]
+ "vt" => Ok(LineEnding::VT),
+ #[cfg(feature = "unicode-lines")]
+ "ff" => Ok(LineEnding::FF),
+ #[cfg(feature = "unicode-lines")]
+ "cr" => Ok(LineEnding::CR),
+ #[cfg(feature = "unicode-lines")]
+ "nel" => Ok(LineEnding::Nel),
+ #[cfg(feature = "unicode-lines")]
+ "ls" => Ok(LineEnding::LS),
+ #[cfg(feature = "unicode-lines")]
+ "ps" => Ok(LineEnding::PS),
+ #[cfg(feature = "unicode-lines")]
+ _ => bail!("expecte one of 'lf', 'crlf', 'vt', 'ff', 'cr', 'nel', 'ls' or 'ps'"),
+ #[cfg(not(feature = "unicode-lines"))]
+ _ => bail!("expecte one of 'lf' or 'crlf'"),
+ }
+ }
+
+ fn to_value(&self) -> helix_config::Value {
+ match self {
+ LineEnding::Crlf => "crlf".into(),
+ LineEnding::LF => "lf".into(),
+ #[cfg(feature = "unicode-lines")]
+ VT => "vt".into(),
+ #[cfg(feature = "unicode-lines")]
+ FF => "ff".into(),
+ #[cfg(feature = "unicode-lines")]
+ CR => "cr".into(),
+ #[cfg(feature = "unicode-lines")]
+ Nel => "nel".into(),
+ #[cfg(feature = "unicode-lines")]
+ LS => "ls".into(),
+ #[cfg(feature = "unicode-lines")]
+ PS => "ps".into(),
+ }
+ }
+}
+
/// Represents one of the valid Unicode line endings.
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
pub enum LineEnding {
diff --git a/helix-dap/Cargo.toml b/helix-dap/Cargo.toml
index f7acb003..3ae4d330 100644
--- a/helix-dap/Cargo.toml
+++ b/helix-dap/Cargo.toml
@@ -14,6 +14,7 @@ homepage.workspace = true
[dependencies]
helix-core = { path = "../helix-core" }
+helix-config = { path = "../helix-config" }
anyhow = "1.0"
log = "0.4"
diff --git a/helix-dap/src/config.rs b/helix-dap/src/config.rs
new file mode 100644
index 00000000..9644c63b
--- /dev/null
+++ b/helix-dap/src/config.rs
@@ -0,0 +1,146 @@
+use anyhow::bail;
+use helix_config::*;
+use serde::{Deserialize, Serialize};
+
+options! {
+ struct DebugAdapterConfig {
+ #[name = "debugger.name"]
+ name: Option<String> = None,
+ #[name = "debugger.transport"]
+ #[read = copy]
+ transport: Transport = Transport::Stdio,
+ #[name = "debugger.command"]
+ #[read = deref]
+ command: String = "",
+ #[name = "debugger.args"]
+ #[read = deref]
+ args: List<String> = List::default(),
+ #[name = "debugger.port-arg"]
+ #[read = deref]
+ port_arg: String = "",
+ #[name = "debugger.templates"]
+ #[read = deref]
+ templates: List<DebugTemplate> = List::default(),
+ #[name = "debugger.quirks.absolut-path"]
+ #[read = copy]
+ absolut_path: bool = false,
+ #[name = "terminal.command"]
+ terminal_command: Option<String> = get_terminal_provider().map(|term| term.command),
+ #[name = "terminal.args"]
+ #[read = deref]
+ terminal_args: List<String> = get_terminal_provider().map(|term| term.args.into_boxed_slice()).unwrap_or_default(),
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum Transport {
+ Stdio,
+ Tcp,
+}
+
+impl Ty for Transport {
+ fn from_value(val: Value) -> anyhow::Result<Self> {
+ match &*String::from_value(val)? {
+ "stdio" => Ok(Transport::Stdio),
+ "tcp" => Ok(Transport::Tcp),
+ val => bail!("expected 'stdio' or 'tcp' (got {val:?})"),
+ }
+ }
+ fn to_value(&self) -> Value {
+ match self {
+ Transport::Stdio => "stdio".into(),
+ Transport::Tcp => "tcp".into(),
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
+#[serde(untagged)]
+pub enum DebugArgumentValue {
+ String(String),
+ Array(Vec<String>),
+ Boolean(bool),
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
+#[serde(rename_all = "kebab-case")]
+pub struct AdvancedCompletion {
+ pub name: Option<String>,
+ pub completion: Option<String>,
+ pub default: Option<String>,
+}
+
+#[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(rename_all = "kebab-case")]
+pub struct DebugTemplate {
+ pub name: String,
+ pub request: String,
+ pub completion: Vec<DebugConfigCompletion>,
+ pub args: Map<DebugArgumentValue>,
+}
+
+// TODO: integrate this better with the new config system (less nesting)
+// the best way to do that is probably a rewrite. I think these templates
+// are probably overkill here. This may be easier to solve by moving the logic
+// to scheme
+config_serde_adapter!(DebugTemplate);
+
+#[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<String>,
+}
+
+#[cfg(windows)]
+pub fn get_terminal_provider() -> Option<TerminalConfig> {
+ use helix_config::env::binary_exists;
+
+ if binary_exists("wt") {
+ return Some(TerminalConfig {
+ command: "wt".into(),
+ args: vec![
+ "new-tab".into(),
+ "--title".into(),
+ "DEBUG".into(),
+ "cmd".into(),
+ "/C".into(),
+ ],
+ });
+ }
+
+ Some(TerminalConfig {
+ command: "conhost".into(),
+ args: vec!["cmd".into(), "/C".into()],
+ })
+}
+
+#[cfg(not(any(windows, target_os = "wasm32")))]
+fn get_terminal_provider() -> Option<TerminalConfig> {
+ use helix_config::env::{binary_exists, env_var_is_set};
+
+ if env_var_is_set("TMUX") && binary_exists("tmux") {
+ return Some(TerminalConfig {
+ command: "tmux".into(),
+ args: vec!["split-window".into()],
+ });
+ }
+
+ if env_var_is_set("WEZTERM_UNIX_SOCKET") && binary_exists("wezterm") {
+ return Some(TerminalConfig {
+ command: "wezterm".into(),
+ args: vec!["cli".into(), "split-pane".into()],
+ });
+ }
+
+ None
+}
diff --git a/helix-dap/src/lib.rs b/helix-dap/src/lib.rs
index 21162cb8..d3ec1e36 100644
--- a/helix-dap/src/lib.rs
+++ b/helix-dap/src/lib.rs
@@ -1,4 +1,5 @@
mod client;
+mod config;
mod transport;
mod types;
diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml
index db53b54c..bae2731c 100644
--- a/helix-view/Cargo.toml
+++ b/helix-view/Cargo.toml
@@ -16,6 +16,7 @@ term = ["crossterm"]
[dependencies]
helix-core = { path = "../helix-core" }
+helix-config = { path = "../helix-config" }
helix-event = { path = "../helix-event" }
helix-loader = { path = "../helix-loader" }
helix-lsp = { path = "../helix-lsp" }
diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs
index ebdac9e2..8c9ab818 100644
--- a/helix-view/src/gutter.rs
+++ b/helix-view/src/gutter.rs
@@ -1,13 +1,96 @@
use std::fmt::Write;
+use helix_config::{config_serde_adapter, options, List};
use helix_core::syntax::LanguageServerFeature;
+use serde::{Deserialize, Serialize};
use crate::{
- editor::GutterType,
graphics::{Style, UnderlineStyle},
Document, Editor, Theme, View,
};
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum LineNumber {
+ /// Show absolute line number
+ #[serde(alias = "abs")]
+ 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.
+ #[serde(alias = "rel")]
+ Relative,
+}
+
+config_serde_adapter!(LineNumber);
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, 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<Self, Self::Err> {
+ 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!(
+ "expected one of `diagnostics`, `spacer`, `line-numbers` or `diff` (found {s:?})"
+ ),
+ }
+ }
+}
+
+impl helix_config::Ty for GutterType {
+ fn from_value(val: helix_config::Value) -> anyhow::Result<Self> {
+ let val: String = val.typed()?;
+ val.parse()
+ }
+
+ fn to_value(&self) -> helix_config::Value {
+ match self {
+ GutterType::Diagnostics => "diagnostics".into(),
+ GutterType::LineNumbers => "lineNumbers".into(),
+ GutterType::Spacer => "spacer".into(),
+ GutterType::Diff => "diff".into(),
+ }
+ }
+}
+
+options! {
+ struct GutterConfig {
+ /// A list of gutters to display
+ #[name = "gutters.layout"]
+ layout: List<GutterType> = &[
+ GutterType::Diagnostics,
+ GutterType::Spacer,
+ GutterType::LineNumbers,
+ GutterType::Spacer,
+ GutterType::Diff,
+ ],
+ /// The minimum number of characters the line number gutter should take up.
+ #[name = "gutters.line-numbers.min-width"]
+ line_number_min_width: usize = 3,
+ /// Line number display: `absolute` simply shows each line's number,
+ /// while `relative` shows the distance from the current line. When
+ /// unfocused or in insert mode, `relative` will still show absolute
+ /// line numbers
+ #[name = "line-number"]
+ line_number_mode: LineNumber = LineNumber::Absolute,
+ }
+}
+
fn count_digits(n: usize) -> usize {
(usize::checked_ilog10(n).unwrap_or(0) + 1) as usize
}