refactor: share language IDs with indentation configuration

This commit is contained in:
Valdemar Lorenzen
2026-10-14 12:11:26 +02:00
parent 6a43fb9e0c
commit 5580d6fbd4
9 changed files with 41 additions and 16 deletions
-3
View File
@@ -3,9 +3,6 @@
- whitespace kind -> character map should be configurable. - whitespace kind -> character map should be configurable.
- [x] add option to insert spaces instead tab character when pressing tab: - [x] add option to insert spaces instead tab character when pressing tab:
- number of spaces should be configurable (per language) - number of spaces should be configurable (per language)
- [ ] diff view - design question:
- is it possible to angle the hatchings for empty lines more?
- if a character like the current one exists with a 45degree angle instead (and a top and bottom), we could use that across two columns. that would give us a perfectly 45 degree hatching.
- [ ] github/gitlab/gitea integration (probably requries authentication with github) - [ ] github/gitlab/gitea integration (probably requries authentication with github)
- github pr picker (floating window) - github pr picker (floating window)
- three tabs: open, closed, all - three tabs: open, closed, all
+2 -2
View File
@@ -20,7 +20,7 @@ Vamp.configure do |config|
config.whitespace_characters = {} # opt in: { " " => "·", "\t" => "→", "\n" => "↵" } config.whitespace_characters = {} # opt in: { " " => "·", "\t" => "→", "\n" => "↵" }
config.whitespace_scope = "all" # or "leading" for indentation only 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.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 = {} # overrides: { "ruby" => 2, "makefile" => nil }
config.file_explorer_side = "right" config.file_explorer_side = "right"
config.file_explorer_view = "tree" config.file_explorer_view = "tree"
config.file_picker_view = "tree" config.file_picker_view = "tree"
@@ -71,7 +71,7 @@ Markers appear in ordinary and historical buffers and both sides of split/unifie
`config.tab_spaces` defaults to `nil`, preserving literal tabs. Set an integer from `1` through `255` to insert that many spaces per Tab in Insert mode, at every caret. This is a fixed count, not alignment to the next tab stop; it does not change how existing tab characters are displayed. `config.tab_spaces` defaults to `nil`, preserving literal tabs. Set an integer from `1` through `255` to insert that many spaces per Tab in Insert mode, at every caret. This is a fixed count, not alignment to the next tab stop; it does not change how existing tab characters are displayed.
`config.language_tab_spaces` defaults to `{}`. It overrides the global setting for the active buffer's detected syntax language, using the case-sensitive names shown in the language bar, for example `{ "Rust" => 4, "Ruby" => 2, "Python" => 4, "Makefile" => nil }`. 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. `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.
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. 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.
+1 -1
View File
@@ -10,7 +10,7 @@ Vamp.configure do |config|
config.whitespace_characters = {} # opt in: { " " => "·", "\t" => "→", "\n" => "↵" } config.whitespace_characters = {} # opt in: { " " => "·", "\t" => "→", "\n" => "↵" }
config.whitespace_scope = "all" # or "leading" for indentation only; newline markers remain independent 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.tab_spaces = nil # opt in: 4 inserts four spaces per Tab
config.language_tab_spaces = {} # overrides: { "Ruby" => 2, "Makefile" => nil } config.language_tab_spaces = {} # overrides: { "ruby" => 2, "makefile" => nil }
config.file_explorer_side = "right" config.file_explorer_side = "right"
config.file_explorer_view = "tree" config.file_explorer_view = "tree"
config.file_picker_view = "tree" config.file_picker_view = "tree"
+1 -1
View File
@@ -8994,7 +8994,7 @@ impl Editor {
let width = if self.language_tab_spaces.is_empty() { let width = if self.language_tab_spaces.is_empty() {
self.tab_spaces self.tab_spaces
} else { } else {
crate::syntax::language_name( crate::syntax::language_id(
self.active.path.as_deref(), self.active.path.as_deref(),
self.active.lines.first().map(String::as_str), self.active.lines.first().map(String::as_str),
) )
+3 -3
View File
@@ -11,9 +11,9 @@ pub fn resolve(value: Value) -> Result<Vec<ServerConfig>, String> {
return Err("LSP language keys must not be empty or contain control characters".into()); return Err("LSP language keys must not be empty or contain control characters".into());
} }
let preset: Option<(&[&[&str]], &str)> = match language_id.as_str() { let preset: Option<(&[&[&str]], &str)> = match language_id.as_str() {
"rust" => Some((&[&["rust-analyzer"]], "rs")), crate::syntax::RUST_ID => Some((&[&["rust-analyzer"]], "rs")),
"python" => Some((&[&["ty", "server"], &["ruff", "server"]], "py")), crate::syntax::PYTHON_ID => Some((&[&["ty", "server"], &["ruff", "server"]], "py")),
"ruby" => Some((&[&["solargraph", "stdio"]], "rb")), crate::syntax::RUBY_ID => Some((&[&["solargraph", "stdio"]], "rb")),
_ => None, _ => None,
}; };
let mut options = match value { let mut options = match value {
+5 -3
View File
@@ -1006,7 +1006,7 @@ mod tests {
r#" r#"
Vamp.configure do |c| Vamp.configure do |c|
c.tab_spaces = 4 c.tab_spaces = 4
c.language_tab_spaces = { "Ruby" => 2, "Makefile" => nil, "JSON" => 1, "YAML" => 255 } c.language_tab_spaces = { "ruby" => 2, "makefile" => nil, "json" => 1, "yaml" => 255, "cpp" => 3, "typescriptreact" => 5 }
end end
"#, "#,
); );
@@ -1021,6 +1021,8 @@ end
("plain.txt", "", " "), ("plain.txt", "", " "),
("buffer.json", "", " "), ("buffer.json", "", " "),
("buffer.yaml", "", widest.as_str()), ("buffer.yaml", "", widest.as_str()),
("buffer.cpp", "", " "),
("buffer.tsx", "", " "),
] { ] {
let source = format!("{prefix}αx\nβy"); let source = format!("{prefix}αx\nβy");
let path = fixture.file(name, &source); let path = fixture.file(name, &source);
@@ -1066,7 +1068,7 @@ end
for value in ["0", "-1", "256", "1.5", "true", r#""4""#, "[]", "{}"] { for value in ["0", "-1", "256", "1.5", "true", r#""4""#, "[]", "{}"] {
for assignment in [ for assignment in [
format!("c.tab_spaces = {value}"), format!("c.tab_spaces = {value}"),
format!(r#"c.language_tab_spaces = {{ "Ruby" => {value} }}"#), format!(r#"c.language_tab_spaces = {{ "ruby" => {value} }}"#),
] { ] {
let config = fixture.file( let config = fixture.file(
"config.rb", "config.rb",
@@ -1079,7 +1081,7 @@ end
"nil", "nil",
"[]", "[]",
r#"{ "" => 2 }"#, r#"{ "" => 2 }"#,
r#"{ :Ruby => 2 }"#, r#"{ :ruby => 2 }"#,
r#"{ "\n" => 2 }"#, r#"{ "\n" => 2 }"#,
] { ] {
let config = fixture.file( let config = fixture.file(
+1 -1
View File
@@ -262,7 +262,7 @@ module Vamp
@tab_spaces = tab_spaces!(spaces) @tab_spaces = tab_spaces!(spaces)
end end
# @param languages [Hash{String => Integer, nil}] syntax language names to Tab widths # @param languages [Hash{String => Integer, nil}] LSP-style language IDs to Tab widths
# @return [Hash{String => Integer, nil}] # @return [Hash{String => Integer, nil}]
def language_tab_spaces=(languages) def language_tab_spaces=(languages)
raise TypeError, "language_tab_spaces must be a Hash" unless languages.is_a?(Hash) raise TypeError, "language_tab_spaces must be a Hash" unless languages.is_a?(Hash)
+7 -2
View File
@@ -9,12 +9,16 @@ use tree_sitter::{
mod languages; mod languages;
use languages::LanguageSpec; use languages::LanguageSpec;
pub(crate) const RUST_ID: &str = "rust";
pub(crate) const RUBY_ID: &str = "ruby";
pub(crate) const PYTHON_ID: &str = "python";
pub(crate) fn is_markdown_path(path: &Path) -> bool { pub(crate) fn is_markdown_path(path: &Path) -> bool {
languages::detect(Some(path), None).is_some_and(|syntax| syntax.name == "Markdown") languages::detect(Some(path), None).is_some_and(|syntax| syntax.name == "Markdown")
} }
pub(crate) fn language_name(path: Option<&Path>, first_line: Option<&str>) -> Option<&'static str> { pub(crate) fn language_id(path: Option<&Path>, first_line: Option<&str>) -> Option<&'static str> {
languages::detect(path, first_line).map(|syntax| syntax.name) languages::detect(path, first_line).map(|syntax| syntax.id)
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -2120,6 +2124,7 @@ mod tests {
"((identifier) @function (#unsupported? @function))", "((identifier) @function (#unsupported? @function))",
] ]
.map(|query| LanguageSpec { .map(|query| LanguageSpec {
id: language.id,
name: "invalid test grammar query", name: "invalid test grammar query",
language: language.language.clone(), language: language.language.clone(),
highlights: query, highlights: query,
+21
View File
@@ -3,6 +3,7 @@ use std::path::Path;
use std::sync::LazyLock; use std::sync::LazyLock;
pub(super) struct LanguageSpec { pub(super) struct LanguageSpec {
pub id: &'static str,
pub name: &'static str, pub name: &'static str,
pub language: tree_sitter::Language, pub language: tree_sitter::Language,
pub highlights: &'static str, pub highlights: &'static str,
@@ -93,6 +94,7 @@ fn javascript_injections() -> &'static str {
} }
fn spec( fn spec(
id: &'static str,
name: &'static str, name: &'static str,
language: tree_sitter::Language, language: tree_sitter::Language,
highlights: &'static str, highlights: &'static str,
@@ -100,6 +102,7 @@ fn spec(
locals: &'static str, locals: &'static str,
) -> LanguageSpec { ) -> LanguageSpec {
LanguageSpec { LanguageSpec {
id,
name, name,
language, language,
highlights, highlights,
@@ -111,6 +114,7 @@ fn spec(
static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| { static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| {
[ [
spec( spec(
super::RUST_ID,
"Rust", "Rust",
tree_sitter_rust::LANGUAGE.into(), tree_sitter_rust::LANGUAGE.into(),
tree_sitter_rust::HIGHLIGHTS_QUERY, tree_sitter_rust::HIGHLIGHTS_QUERY,
@@ -118,6 +122,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| {
"", "",
), ),
spec( spec(
super::RUBY_ID,
"Ruby", "Ruby",
tree_sitter_ruby::LANGUAGE.into(), tree_sitter_ruby::LANGUAGE.into(),
tree_sitter_ruby::HIGHLIGHTS_QUERY, tree_sitter_ruby::HIGHLIGHTS_QUERY,
@@ -125,6 +130,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| {
tree_sitter_ruby::LOCALS_QUERY, tree_sitter_ruby::LOCALS_QUERY,
), ),
spec( spec(
super::PYTHON_ID,
"Python", "Python",
tree_sitter_python::LANGUAGE.into(), tree_sitter_python::LANGUAGE.into(),
tree_sitter_python::HIGHLIGHTS_QUERY, tree_sitter_python::HIGHLIGHTS_QUERY,
@@ -132,6 +138,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| {
"", "",
), ),
spec( spec(
"json",
"JSON", "JSON",
tree_sitter_json::LANGUAGE.into(), tree_sitter_json::LANGUAGE.into(),
tree_sitter_json::HIGHLIGHTS_QUERY, tree_sitter_json::HIGHLIGHTS_QUERY,
@@ -139,6 +146,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| {
"", "",
), ),
spec( spec(
"shellscript",
"Bash", "Bash",
tree_sitter_bash::LANGUAGE.into(), tree_sitter_bash::LANGUAGE.into(),
tree_sitter_bash::HIGHLIGHT_QUERY, tree_sitter_bash::HIGHLIGHT_QUERY,
@@ -146,6 +154,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| {
"", "",
), ),
spec( spec(
"makefile",
"Makefile", "Makefile",
tree_sitter_make::LANGUAGE.into(), tree_sitter_make::LANGUAGE.into(),
tree_sitter_make::HIGHLIGHTS_QUERY, tree_sitter_make::HIGHLIGHTS_QUERY,
@@ -153,6 +162,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| {
"", "",
), ),
spec( spec(
"markdown",
"Markdown", "Markdown",
tree_sitter_md::LANGUAGE.into(), tree_sitter_md::LANGUAGE.into(),
MD_HIGHLIGHTS.as_str(), MD_HIGHLIGHTS.as_str(),
@@ -160,6 +170,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| {
"", "",
), ),
spec( spec(
"javascript",
"JavaScript", "JavaScript",
tree_sitter_javascript::LANGUAGE.into(), tree_sitter_javascript::LANGUAGE.into(),
JS_HIGHLIGHTS.as_str(), JS_HIGHLIGHTS.as_str(),
@@ -167,6 +178,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| {
tree_sitter_javascript::LOCALS_QUERY, tree_sitter_javascript::LOCALS_QUERY,
), ),
spec( spec(
"typescript",
"TypeScript", "TypeScript",
tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(), tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
TS_HIGHLIGHTS.as_str(), TS_HIGHLIGHTS.as_str(),
@@ -174,6 +186,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| {
TS_LOCALS.as_str(), TS_LOCALS.as_str(),
), ),
spec( spec(
"html",
"HTML", "HTML",
tree_sitter_html::LANGUAGE.into(), tree_sitter_html::LANGUAGE.into(),
tree_sitter_html::HIGHLIGHTS_QUERY, tree_sitter_html::HIGHLIGHTS_QUERY,
@@ -181,6 +194,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| {
"", "",
), ),
spec( spec(
"css",
"CSS", "CSS",
tree_sitter_css::LANGUAGE.into(), tree_sitter_css::LANGUAGE.into(),
tree_sitter_css::HIGHLIGHTS_QUERY, tree_sitter_css::HIGHLIGHTS_QUERY,
@@ -188,6 +202,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| {
"", "",
), ),
spec( spec(
"yaml",
"YAML", "YAML",
tree_sitter_yaml::LANGUAGE.into(), tree_sitter_yaml::LANGUAGE.into(),
tree_sitter_yaml::HIGHLIGHTS_QUERY, tree_sitter_yaml::HIGHLIGHTS_QUERY,
@@ -195,6 +210,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| {
"", "",
), ),
spec( spec(
"toml",
"TOML", "TOML",
tree_sitter_toml_ng::LANGUAGE.into(), tree_sitter_toml_ng::LANGUAGE.into(),
tree_sitter_toml_ng::HIGHLIGHTS_QUERY, tree_sitter_toml_ng::HIGHLIGHTS_QUERY,
@@ -202,6 +218,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| {
"", "",
), ),
spec( spec(
"c",
"C", "C",
tree_sitter_c::LANGUAGE.into(), tree_sitter_c::LANGUAGE.into(),
tree_sitter_c::HIGHLIGHT_QUERY, tree_sitter_c::HIGHLIGHT_QUERY,
@@ -209,6 +226,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| {
"", "",
), ),
spec( spec(
"cpp",
"C++", "C++",
tree_sitter_cpp::LANGUAGE.into(), tree_sitter_cpp::LANGUAGE.into(),
CPP_HIGHLIGHTS.as_str(), CPP_HIGHLIGHTS.as_str(),
@@ -216,6 +234,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| {
"", "",
), ),
spec( spec(
"go",
"Go", "Go",
tree_sitter_go::LANGUAGE.into(), tree_sitter_go::LANGUAGE.into(),
tree_sitter_go::HIGHLIGHTS_QUERY, tree_sitter_go::HIGHLIGHTS_QUERY,
@@ -223,6 +242,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| {
"", "",
), ),
spec( spec(
"typescriptreact",
"TypeScript", "TypeScript",
tree_sitter_typescript::LANGUAGE_TSX.into(), tree_sitter_typescript::LANGUAGE_TSX.into(),
TSX_HIGHLIGHTS.as_str(), TSX_HIGHLIGHTS.as_str(),
@@ -230,6 +250,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| {
TS_LOCALS.as_str(), TS_LOCALS.as_str(),
), ),
spec( spec(
"markdown",
"Markdown inline", "Markdown inline",
tree_sitter_md::INLINE_LANGUAGE.into(), tree_sitter_md::INLINE_LANGUAGE.into(),
tree_sitter_md::HIGHLIGHT_QUERY_INLINE, tree_sitter_md::HIGHLIGHT_QUERY_INLINE,