From 8a1e98819cfad530b154a8830fa1ad383d85c244 Mon Sep 17 00:00:00 2001 From: Valdemar Lorenzen Date: Wed, 14 Oct 2026 12:31:44 +0200 Subject: [PATCH] fix: support mutable language tab configuration --- docs/configuration.md | 13 ++++++++++++- examples/config.rb | 2 +- src/ruby.rs | 29 +++++++++++++++++++++++++++-- src/ruby_config.rb | 27 ++++++++++++++++++--------- 4 files changed, 58 insertions(+), 13 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index e838f7c..79e03bb 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -20,7 +20,7 @@ Vamp.configure do |config| config.whitespace_characters = {} # opt in: { " " => "·", "\t" => "→", "\n" => "↵" } config.whitespace_scope = "all" # or "leading" for indentation only config.tab_spaces = nil # opt in with a fixed number of spaces per Tab, e.g. 4 - config.language_tab_spaces = {} # overrides: { "ruby" => 2, "makefile" => nil } + # config.language_tab_spaces.merge!("ruby" => 2, "makefile" => nil) config.file_explorer_side = "right" config.file_explorer_view = "tree" config.file_picker_view = "tree" @@ -73,6 +73,17 @@ Markers appear in ordinary and historical buffers and both sides of split/unifie `config.language_tab_spaces` defaults to `{}`. It overrides the global setting using the same case-sensitive language IDs as `config.lsp`, for example `{ "rust" => 4, "ruby" => 2, "python" => 4, "makefile" => nil }`. Syntax detection and built-in LSP presets share canonical IDs rather than maintaining separate naming conventions; other bundled IDs include `shellscript`, `cpp`, and `typescriptreact` for Bash, C++, and TSX. Display labels such as “Ruby” remain presentation-only. A `nil` override preserves literal tabs for that language, even when the global setting uses spaces. Unlisted or unknown languages and unnamed buffers without a recognized shebang use `tab_spaces`. Detection reuses syntax highlighting's filename, extension, and shebang rules, independently of whether an LSP is enabled. +Like `config.lsp`, `config.language_tab_spaces` exposes a mutable hash: use `merge!` to add or override entries, or `config.language_tab_spaces["ruby"] = 2` to change one entry. Whole-hash assignment with `=` remains supported. Both assignment and the final hash contents are validated; invalid keys or widths inserted through hash mutation reject startup configuration rather than bypassing validation. + +```ruby +config.tab_spaces = 4 +config.language_tab_spaces.merge!( + "rust" => 4, + "ruby" => 2, + "makefile" => nil, +) +``` + Active snippets retain Tab/Shift+Tab argument navigation; after the final stop, Tab uses the configured insertion setting. Explicit Tab bindings still take precedence, and Ctrl+i remains a Tab alias. Bracketed paste, clipboard paste, completion payloads, and plugin text retain literal tab characters. Insert undo/redo and dot repeat preserve the resolved spaces or tab. Widths other than `nil` or integers in `1..255`, non-hash language overrides, and empty, non-string, or control-containing language keys reject the configuration. `config.git_blame` defaults to `true`; set it to `false` to disable current-line inline Git blame and its background worker. Only literal Ruby booleans are accepted; other values reject the configuration. Toggle it for the running session with `:git_blame.toggle`, its palette action, or a custom binding such as `config.bind "leader b", config.core.git_blame.toggle`. There is no default shortcut. diff --git a/examples/config.rb b/examples/config.rb index 48a80e5..f278dde 100644 --- a/examples/config.rb +++ b/examples/config.rb @@ -10,7 +10,7 @@ Vamp.configure do |config| config.whitespace_characters = {} # opt in: { " " => "·", "\t" => "→", "\n" => "↵" } config.whitespace_scope = "all" # or "leading" for indentation only; newline markers remain independent config.tab_spaces = nil # opt in: 4 inserts four spaces per Tab - config.language_tab_spaces = {} # overrides: { "ruby" => 2, "makefile" => nil } + # config.language_tab_spaces.merge!("ruby" => 2, "makefile" => nil) config.file_explorer_side = "right" config.file_explorer_view = "tree" config.file_picker_view = "tree" diff --git a/src/ruby.rs b/src/ruby.rs index 5deee41..867582e 100644 --- a/src/ruby.rs +++ b/src/ruby.rs @@ -1006,7 +1006,8 @@ mod tests { r#" Vamp.configure do |c| c.tab_spaces = 4 - c.language_tab_spaces = { "ruby" => 2, "makefile" => nil, "json" => 1, "yaml" => 255, "cpp" => 3, "typescriptreact" => 5 } + c.language_tab_spaces.merge!("ruby" => 2, "makefile" => nil, "json" => 1, "yaml" => 255, "typescriptreact" => 5) + c.language_tab_spaces["cpp"] = 3 end "#, ); @@ -1069,12 +1070,18 @@ end for assignment in [ format!("c.tab_spaces = {value}"), format!(r#"c.language_tab_spaces = {{ "ruby" => {value} }}"#), + format!(r#"c.language_tab_spaces.merge!("ruby" => {value})"#), + format!(r#"c.language_tab_spaces["ruby"] = {value}"#), ] { let config = fixture.file( "config.rb", &format!("Vamp.configure {{ |c| {assignment} }}"), ); - assert!(load_config(&config).is_err(), "{assignment}"); + let error = load_config(&config).unwrap_err(); + assert!( + error.contains("tab_spaces must be nil or an Integer"), + "{assignment}: {error}" + ); } } for value in [ @@ -1092,6 +1099,24 @@ end } } + #[test] + fn language_tab_space_mutations_reject_invalid_keys_before_serialization() { + let fixture = Fixture::new(); + for key in [r#""""#, ":ruby", r#""\n""#, "7"] { + for assignment in [ + format!("c.language_tab_spaces.merge!({key} => 2)"), + format!("c.language_tab_spaces[{key}] = 2"), + ] { + let config = fixture.file( + "config.rb", + &format!("Vamp.configure {{ |c| {assignment} }}"), + ); + let error = load_config(&config).unwrap_err(); + assert!(error.contains("Tab language"), "{assignment}: {error}"); + } + } + } + #[test] fn actual_ruby_dsl_context_queued_effects_and_output_isolation() { let fixture = Fixture::new(); diff --git a/src/ruby_config.rb b/src/ruby_config.rb index 8f200af..7ef3116 100644 --- a/src/ruby_config.rb +++ b/src/ruby_config.rb @@ -120,6 +120,9 @@ module Vamp # @return [Hash{String => Boolean, Hash{String, Symbol => Object}}] language presets/options attr_reader :lsp + # @return [Hash{String => Integer, nil}] LSP-style language IDs to Tab widths + attr_reader :language_tab_spaces + # @return [Vamp::BarConfig] attr_reader :bar @@ -265,14 +268,7 @@ module Vamp # @param languages [Hash{String => Integer, nil}] LSP-style language IDs to Tab widths # @return [Hash{String => Integer, nil}] def language_tab_spaces=(languages) - raise TypeError, "language_tab_spaces must be a Hash" unless languages.is_a?(Hash) - - @language_tab_spaces = languages.to_h do |language, spaces| - language = nonempty_string!(language, "Tab language") - raise ArgumentError, "Tab language must not contain control characters" if language.match?(/[[:cntrl:]]/) - - [language, tab_spaces!(spaces)] - end + @language_tab_spaces = language_tab_spaces!(languages) end # @param lines [Integer] nonnegative context lines around each diff change @@ -350,7 +346,7 @@ module Vamp "highlight_trailing_whitespace" => @highlight_trailing_whitespace, "whitespace_characters" => @whitespace_characters, "whitespace_scope" => @whitespace_scope, - "tab_spaces" => @tab_spaces, "language_tab_spaces" => @language_tab_spaces, + "tab_spaces" => @tab_spaces, "language_tab_spaces" => language_tab_spaces!(@language_tab_spaces), "top_bar" => @bar.top, "bottom_bar" => @bar.bottom, "labels" => @labels, "popup" => { "command" => @popup.command, "search" => @popup.search }, "diff_context_lines" => @diff_context_lines, "diff_expand_lines" => @diff_expand_lines, @@ -369,6 +365,19 @@ module Vamp spaces end + # @param languages [Hash{String => Integer, nil}] + # @return [Hash{String => Integer, nil}] + def language_tab_spaces!(languages) + raise TypeError, "language_tab_spaces must be a Hash" unless languages.is_a?(Hash) + + languages.to_h do |language, spaces| + language = nonempty_string!(language, "Tab language") + raise ArgumentError, "Tab language must not contain control characters" if language.match?(/[[:cntrl:]]/) + + [language, tab_spaces!(spaces)] + end + end + # @param value [Integer] # @param label [String] # @param minimum [Integer]