From 5580d6fbd4ffa9b20c3053cbe1360d8cf68f8013 Mon Sep 17 00:00:00 2001 From: Valdemar Lorenzen Date: Wed, 14 Oct 2026 12:11:26 +0200 Subject: [PATCH] refactor: share language IDs with indentation configuration --- TODO.md | 3 --- docs/configuration.md | 4 ++-- examples/config.rb | 2 +- src/editor.rs | 2 +- src/lsp_config.rs | 6 +++--- src/ruby.rs | 8 +++++--- src/ruby_config.rb | 2 +- src/syntax.rs | 9 +++++++-- src/syntax/languages.rs | 21 +++++++++++++++++++++ 9 files changed, 41 insertions(+), 16 deletions(-) diff --git a/TODO.md b/TODO.md index 54902a9..c6f0b31 100644 --- a/TODO.md +++ b/TODO.md @@ -3,9 +3,6 @@ - whitespace kind -> character map should be configurable. - [x] add option to insert spaces instead tab character when pressing tab: - 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 pr picker (floating window) - three tabs: open, closed, all diff --git a/docs/configuration.md b/docs/configuration.md index 6af8faa..215d19b 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 = {} # overrides: { "ruby" => 2, "makefile" => nil } config.file_explorer_side = "right" config.file_explorer_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.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. diff --git a/examples/config.rb b/examples/config.rb index 0dd7aca..48a80e5 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 = {} # overrides: { "ruby" => 2, "makefile" => nil } config.file_explorer_side = "right" config.file_explorer_view = "tree" config.file_picker_view = "tree" diff --git a/src/editor.rs b/src/editor.rs index 625e182..0a827c3 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -8994,7 +8994,7 @@ impl Editor { let width = if self.language_tab_spaces.is_empty() { self.tab_spaces } else { - crate::syntax::language_name( + crate::syntax::language_id( self.active.path.as_deref(), self.active.lines.first().map(String::as_str), ) diff --git a/src/lsp_config.rs b/src/lsp_config.rs index b418732..19fd37b 100644 --- a/src/lsp_config.rs +++ b/src/lsp_config.rs @@ -11,9 +11,9 @@ pub fn resolve(value: Value) -> Result, String> { return Err("LSP language keys must not be empty or contain control characters".into()); } let preset: Option<(&[&[&str]], &str)> = match language_id.as_str() { - "rust" => Some((&[&["rust-analyzer"]], "rs")), - "python" => Some((&[&["ty", "server"], &["ruff", "server"]], "py")), - "ruby" => Some((&[&["solargraph", "stdio"]], "rb")), + crate::syntax::RUST_ID => Some((&[&["rust-analyzer"]], "rs")), + crate::syntax::PYTHON_ID => Some((&[&["ty", "server"], &["ruff", "server"]], "py")), + crate::syntax::RUBY_ID => Some((&[&["solargraph", "stdio"]], "rb")), _ => None, }; let mut options = match value { diff --git a/src/ruby.rs b/src/ruby.rs index db0138c..5deee41 100644 --- a/src/ruby.rs +++ b/src/ruby.rs @@ -1006,7 +1006,7 @@ mod tests { r#" Vamp.configure do |c| 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 "#, ); @@ -1021,6 +1021,8 @@ end ("plain.txt", "", " "), ("buffer.json", "", " "), ("buffer.yaml", "", widest.as_str()), + ("buffer.cpp", "", " "), + ("buffer.tsx", "", " "), ] { let source = format!("{prefix}αx\nβy"); let path = fixture.file(name, &source); @@ -1066,7 +1068,7 @@ end 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} }}"#), + format!(r#"c.language_tab_spaces = {{ "ruby" => {value} }}"#), ] { let config = fixture.file( "config.rb", @@ -1079,7 +1081,7 @@ end "nil", "[]", r#"{ "" => 2 }"#, - r#"{ :Ruby => 2 }"#, + r#"{ :ruby => 2 }"#, r#"{ "\n" => 2 }"#, ] { let config = fixture.file( diff --git a/src/ruby_config.rb b/src/ruby_config.rb index 67ff2d1..8f200af 100644 --- a/src/ruby_config.rb +++ b/src/ruby_config.rb @@ -262,7 +262,7 @@ module Vamp @tab_spaces = tab_spaces!(spaces) 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}] def language_tab_spaces=(languages) raise TypeError, "language_tab_spaces must be a Hash" unless languages.is_a?(Hash) diff --git a/src/syntax.rs b/src/syntax.rs index c1c877a..a5f0040 100644 --- a/src/syntax.rs +++ b/src/syntax.rs @@ -9,12 +9,16 @@ use tree_sitter::{ mod languages; 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 { 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> { - languages::detect(path, first_line).map(|syntax| syntax.name) +pub(crate) fn language_id(path: Option<&Path>, first_line: Option<&str>) -> Option<&'static str> { + languages::detect(path, first_line).map(|syntax| syntax.id) } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -2120,6 +2124,7 @@ mod tests { "((identifier) @function (#unsupported? @function))", ] .map(|query| LanguageSpec { + id: language.id, name: "invalid test grammar query", language: language.language.clone(), highlights: query, diff --git a/src/syntax/languages.rs b/src/syntax/languages.rs index 899d116..6900893 100644 --- a/src/syntax/languages.rs +++ b/src/syntax/languages.rs @@ -3,6 +3,7 @@ use std::path::Path; use std::sync::LazyLock; pub(super) struct LanguageSpec { + pub id: &'static str, pub name: &'static str, pub language: tree_sitter::Language, pub highlights: &'static str, @@ -93,6 +94,7 @@ fn javascript_injections() -> &'static str { } fn spec( + id: &'static str, name: &'static str, language: tree_sitter::Language, highlights: &'static str, @@ -100,6 +102,7 @@ fn spec( locals: &'static str, ) -> LanguageSpec { LanguageSpec { + id, name, language, highlights, @@ -111,6 +114,7 @@ fn spec( static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| { [ spec( + super::RUST_ID, "Rust", tree_sitter_rust::LANGUAGE.into(), tree_sitter_rust::HIGHLIGHTS_QUERY, @@ -118,6 +122,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| { "", ), spec( + super::RUBY_ID, "Ruby", tree_sitter_ruby::LANGUAGE.into(), tree_sitter_ruby::HIGHLIGHTS_QUERY, @@ -125,6 +130,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| { tree_sitter_ruby::LOCALS_QUERY, ), spec( + super::PYTHON_ID, "Python", tree_sitter_python::LANGUAGE.into(), tree_sitter_python::HIGHLIGHTS_QUERY, @@ -132,6 +138,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| { "", ), spec( + "json", "JSON", tree_sitter_json::LANGUAGE.into(), tree_sitter_json::HIGHLIGHTS_QUERY, @@ -139,6 +146,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| { "", ), spec( + "shellscript", "Bash", tree_sitter_bash::LANGUAGE.into(), tree_sitter_bash::HIGHLIGHT_QUERY, @@ -146,6 +154,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| { "", ), spec( + "makefile", "Makefile", tree_sitter_make::LANGUAGE.into(), tree_sitter_make::HIGHLIGHTS_QUERY, @@ -153,6 +162,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| { "", ), spec( + "markdown", "Markdown", tree_sitter_md::LANGUAGE.into(), MD_HIGHLIGHTS.as_str(), @@ -160,6 +170,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| { "", ), spec( + "javascript", "JavaScript", tree_sitter_javascript::LANGUAGE.into(), JS_HIGHLIGHTS.as_str(), @@ -167,6 +178,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| { tree_sitter_javascript::LOCALS_QUERY, ), spec( + "typescript", "TypeScript", tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(), TS_HIGHLIGHTS.as_str(), @@ -174,6 +186,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| { TS_LOCALS.as_str(), ), spec( + "html", "HTML", tree_sitter_html::LANGUAGE.into(), tree_sitter_html::HIGHLIGHTS_QUERY, @@ -181,6 +194,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| { "", ), spec( + "css", "CSS", tree_sitter_css::LANGUAGE.into(), tree_sitter_css::HIGHLIGHTS_QUERY, @@ -188,6 +202,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| { "", ), spec( + "yaml", "YAML", tree_sitter_yaml::LANGUAGE.into(), tree_sitter_yaml::HIGHLIGHTS_QUERY, @@ -195,6 +210,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| { "", ), spec( + "toml", "TOML", tree_sitter_toml_ng::LANGUAGE.into(), tree_sitter_toml_ng::HIGHLIGHTS_QUERY, @@ -202,6 +218,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| { "", ), spec( + "c", "C", tree_sitter_c::LANGUAGE.into(), tree_sitter_c::HIGHLIGHT_QUERY, @@ -209,6 +226,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| { "", ), spec( + "cpp", "C++", tree_sitter_cpp::LANGUAGE.into(), CPP_HIGHLIGHTS.as_str(), @@ -216,6 +234,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| { "", ), spec( + "go", "Go", tree_sitter_go::LANGUAGE.into(), tree_sitter_go::HIGHLIGHTS_QUERY, @@ -223,6 +242,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| { "", ), spec( + "typescriptreact", "TypeScript", tree_sitter_typescript::LANGUAGE_TSX.into(), TSX_HIGHLIGHTS.as_str(), @@ -230,6 +250,7 @@ static LANGUAGES: LazyLock<[LanguageSpec; 18]> = LazyLock::new(|| { TS_LOCALS.as_str(), ), spec( + "markdown", "Markdown inline", tree_sitter_md::INLINE_LANGUAGE.into(), tree_sitter_md::HIGHLIGHT_QUERY_INLINE,