feat: configure spaces on Tab per language

This commit is contained in:
Valdemar Lorenzen
2026-10-14 12:02:19 +02:00
parent 86718221c4
commit 6a43fb9e0c
10 changed files with 200 additions and 8 deletions
+112 -3
View File
@@ -3,6 +3,7 @@ use crate::editor::{Context, Effect, Feature};
use crate::plugin::{DocumentEvent, PluginEvent, PluginUpdate, UpdateEffect};
use serde::{Deserialize, Serialize};
use std::io::{self, BufRead, BufReader, Write};
use std::num::NonZeroU8;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::sync::mpsc::{self, Receiver, RecvTimeoutError, SyncSender};
@@ -48,6 +49,8 @@ pub struct Config {
pub highlight_trailing_whitespace: bool,
pub whitespace_characters: BTreeMap<char, char>,
pub whitespace_scope: crate::editor::WhitespaceScope,
pub tab_spaces: Option<NonZeroU8>,
pub language_tab_spaces: BTreeMap<String, Option<NonZeroU8>>,
pub diff_context_lines: usize,
pub diff_expand_lines: usize,
pub diff_view_mode: crate::diff::ViewMode,
@@ -97,6 +100,8 @@ impl Default for Config {
highlight_trailing_whitespace: false,
whitespace_characters: BTreeMap::new(),
whitespace_scope: crate::editor::WhitespaceScope::All,
tab_spaces: None,
language_tab_spaces: BTreeMap::new(),
diff_context_lines: 10,
diff_expand_lines: 15,
diff_view_mode: crate::diff::ViewMode::Split,
@@ -133,6 +138,8 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
highlight_trailing_whitespace,
whitespace_characters,
whitespace_scope,
tab_spaces,
language_tab_spaces,
diff_context_lines,
diff_expand_lines,
diff_view_mode,
@@ -170,6 +177,8 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
config.highlight_trailing_whitespace = highlight_trailing_whitespace;
config.whitespace_characters = whitespace_characters;
config.whitespace_scope = whitespace_scope;
config.tab_spaces = tab_spaces;
config.language_tab_spaces = language_tab_spaces;
config.diff_context_lines = diff_context_lines;
config.diff_expand_lines = diff_expand_lines;
config.diff_view_mode = diff_view_mode;
@@ -507,6 +516,9 @@ struct ConfigFrame {
#[serde(deserialize_with = "whitespace_characters")]
whitespace_characters: BTreeMap<char, char>,
whitespace_scope: crate::editor::WhitespaceScope,
#[serde(deserialize_with = "Option::deserialize")]
tab_spaces: Option<NonZeroU8>,
language_tab_spaces: BTreeMap<String, Option<NonZeroU8>>,
#[serde(deserialize_with = "diff_context_lines")]
diff_context_lines: usize,
#[serde(deserialize_with = "diff_expand_lines")]
@@ -986,6 +998,98 @@ mod tests {
}
}
#[test]
fn tab_spaces_follow_active_language_and_preserve_native_edit_semantics() {
let fixture = Fixture::new();
let config = fixture.file(
"config.rb",
r#"
Vamp.configure do |c|
c.tab_spaces = 4
c.language_tab_spaces = { "Ruby" => 2, "Makefile" => nil, "JSON" => 1, "YAML" => 255 }
end
"#,
);
let widest = " ".repeat(255);
for (name, prefix, inserted) in [
("default.rs", "", "\t"),
("buffer.rs", "", " "),
("buffer.rb", "", " "),
("Gemfile", "", " "),
("script", "#!/usr/bin/env ruby\n", " "),
("Makefile", "", "\t"),
("plain.txt", "", " "),
("buffer.json", "", " "),
("buffer.yaml", "", widest.as_str()),
] {
let source = format!("{prefix}αx\nβy");
let path = fixture.file(name, &source);
let config = (name != "default.rs").then_some(config.as_path());
let mut session = EditorSession::open(Some(path), vec![], config).unwrap();
assert!(session.editor().startup_diagnostic().is_none());
if !prefix.is_empty() {
session.handle(Input::Char('j'));
}
session.handle(Input::Ctrl('e'));
for input in [
Input::Char('l'),
Input::Char('i'),
Input::Char('\t'),
Input::Ctrl('i'),
Input::Escape,
] {
session.handle(input);
}
let expected = format!("{prefix}α{inserted}{inserted}x\nβ{inserted}{inserted}y");
assert_eq!(session.editor().context().text, expected, "{name}");
session.handle(Input::Char('u'));
assert_eq!(session.editor().context().text, source, "{name}");
session.handle(Input::Char('U'));
assert_eq!(session.editor().context().text, expected, "{name}");
session.handle(Input::Char('u'));
session.handle(Input::Char('.'));
assert_eq!(session.editor().context().text, expected, "{name}");
session.handle(Input::Char('i'));
session.handle(Input::End);
session.handle(Input::Paste("\t".into()));
assert_eq!(
session.editor().context().text,
format!("{prefix}α{inserted}{inserted}x\t\nβ{inserted}{inserted}y\t"),
"{name}"
);
}
}
#[test]
fn tab_spaces_reject_invalid_widths_and_language_maps() {
let fixture = Fixture::new();
for value in ["0", "-1", "256", "1.5", "true", r#""4""#, "[]", "{}"] {
for assignment in [
format!("c.tab_spaces = {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}");
}
}
for value in [
"nil",
"[]",
r#"{ "" => 2 }"#,
r#"{ :Ruby => 2 }"#,
r#"{ "\n" => 2 }"#,
] {
let config = fixture.file(
"config.rb",
&format!("Vamp.configure {{ |c| c.language_tab_spaces = {value} }}"),
);
assert!(load_config(&config).is_err(), "{value}");
}
}
#[test]
fn actual_ruby_dsl_context_queued_effects_and_output_isolation() {
let fixture = Fixture::new();
@@ -1274,9 +1378,9 @@ end
assert!(load_config(&path).unwrap_err().contains(error), "{value}");
}
for frame in [
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","scroll_distance":0,"highlight_trailing_whitespace":false,"whitespace_characters":{},"whitespace_scope":"all","top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true}"#,
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"leader":null,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","scroll_distance":0,"highlight_trailing_whitespace":false,"whitespace_characters":{},"whitespace_scope":"all","top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true}"#,
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"leader":7,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","scroll_distance":0,"highlight_trailing_whitespace":false,"whitespace_characters":{},"whitespace_scope":"all","top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true}"#,
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","scroll_distance":0,"highlight_trailing_whitespace":false,"whitespace_characters":{},"whitespace_scope":"all","tab_spaces":null,"language_tab_spaces":{},"top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true}"#,
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"leader":null,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","scroll_distance":0,"highlight_trailing_whitespace":false,"whitespace_characters":{},"whitespace_scope":"all","tab_spaces":null,"language_tab_spaces":{},"top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true}"#,
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"leader":7,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","scroll_distance":0,"highlight_trailing_whitespace":false,"whitespace_characters":{},"whitespace_scope":"all","tab_spaces":null,"language_tab_spaces":{},"top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true}"#,
] {
assert!(serde_json::from_str::<Response>(frame).is_err(), "{frame}");
}
@@ -1356,6 +1460,7 @@ end
"highlight_trailing_whitespace": false,
"whitespace_characters": {},
"whitespace_scope": "all",
"tab_spaces": null, "language_tab_spaces": {},
"top_bar": {}, "bottom_bar": {}, "popup": {"command": {}, "search": {}},
"labels": {}, "diff_context_lines": 10, "diff_expand_lines": 15,
"diff_view_mode": "split", "diff_split_orientation": "vertical",
@@ -1494,6 +1599,7 @@ end"#,
"file_explorer_view": "tree", "clipboard_mode": "system", "git_blame": true,
"git_gutter": "bar", "scroll_distance": 0, "highlight_trailing_whitespace": false,
"whitespace_characters": {}, "whitespace_scope": "all",
"tab_spaces": null, "language_tab_spaces": {},
"top_bar": {}, "bottom_bar": {}, "popup": {"command": {}, "search": {}},
"labels": {}, "diff_context_lines": 10, "diff_expand_lines": 15,
"diff_view_mode": "split", "diff_split_orientation": "vertical",
@@ -1755,6 +1861,7 @@ end
"highlight_trailing_whitespace": false,
"whitespace_characters": {},
"whitespace_scope": "all",
"tab_spaces": null, "language_tab_spaces": {},
"popup": {"command": {}, "search": {}},
"clipboard_mode": "system", "diff_context_lines": 0, "diff_expand_lines": 7,
"diff_view_mode": "split", "diff_split_orientation": "vertical",
@@ -1806,6 +1913,7 @@ end
"highlight_trailing_whitespace": false,
"whitespace_characters": {},
"whitespace_scope": "all",
"tab_spaces": null, "language_tab_spaces": {},
"top_bar": {}, "bottom_bar": {}, "labels": {},
"popup": {"command": {}, "search": {}},
"diff_context_lines": 10, "diff_expand_lines": 15,
@@ -2237,6 +2345,7 @@ end"#,
"highlight_trailing_whitespace": false,
"whitespace_characters": {},
"whitespace_scope": "all",
"tab_spaces": null, "language_tab_spaces": {},
"top_bar": {}, "bottom_bar": {}, "labels": {},
"popup": {"command": {}, "search": {}},
"clipboard_mode": "system", "diff_context_lines": 10, "diff_expand_lines": 15,