From 1f791ac435e47c78dc0880ff1bed2063aafc6df8 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Sun, 9 Aug 2026 20:04:44 +0200 Subject: [PATCH] Support strict locals highlighting in ERB files --- languages/erb/injections.scm | 5 +++ languages/html-erb/injections.scm | 5 +++ languages/js-erb/injections.scm | 5 +++ languages/ruby/highlights.scm | 22 +++++++++++ languages/yaml-erb/injections.scm | 5 +++ tests/erb.rs | 39 +++++++++++++++++++ tests/languages/erb/injections.erb | 2 + tests/languages/erb/snapshots/injections.snap | 18 ++++++--- 8 files changed, 96 insertions(+), 5 deletions(-) diff --git a/languages/erb/injections.scm b/languages/erb/injections.scm index 5b41cc4..31b4bce 100644 --- a/languages/erb/injections.scm +++ b/languages/erb/injections.scm @@ -7,5 +7,10 @@ (#set! "combined")) ((comment) @content + (#match? @content "^\\s*locals:\\s+\\(") + (#set! "language" "ruby")) + +((comment) @content + (#not-match? @content "^\\s*locals:\\s+\\(") (#set! injection.language "comment") (#set! "combined")) diff --git a/languages/html-erb/injections.scm b/languages/html-erb/injections.scm index 1350c57..48765d4 100644 --- a/languages/html-erb/injections.scm +++ b/languages/html-erb/injections.scm @@ -7,5 +7,10 @@ (#set! "combined")) ((comment) @content + (#match? @content "^\\s*locals:\\s+\\(") + (#set! "language" "ruby")) + +((comment) @content + (#not-match? @content "^\\s*locals:\\s+\\(") (#set! injection.language "comment") (#set! "combined")) diff --git a/languages/js-erb/injections.scm b/languages/js-erb/injections.scm index d14cbec..71fb7db 100644 --- a/languages/js-erb/injections.scm +++ b/languages/js-erb/injections.scm @@ -7,5 +7,10 @@ (#set! "combined")) ((comment) @content + (#match? @content "^\\s*locals:\\s+\\(") + (#set! "language" "ruby")) + +((comment) @content + (#not-match? @content "^\\s*locals:\\s+\\(") (#set! injection.language "comment") (#set! "combined")) diff --git a/languages/ruby/highlights.scm b/languages/ruby/highlights.scm index 139c872..58cfabe 100644 --- a/languages/ruby/highlights.scm +++ b/languages/ruby/highlights.scm @@ -104,6 +104,28 @@ (block_parameters (identifier) @variable.parameter) +; ERB strict locals are injected as Ruby, but their parameter list is not a +; valid standalone Ruby program. Match the parser's recovery shape so the +; first required local and the remaining keyword locals are highlighted alike. +((call + method: (identifier) @_locals + arguments: (argument_list + (parenthesized_statements + (call + method: (identifier) @variable.parameter.keyword)))) + (#eq? @_locals "locals")) + +((call + method: (identifier) @_locals + arguments: (argument_list + (parenthesized_statements + (call + arguments: (argument_list + (pair + key: (hash_key_symbol) @variable.parameter.keyword)))))) + (#eq? @_locals "locals") + (#not-eq? @variable.parameter.keyword "")) + ; Identifiers ((identifier) @constant.builtin (#match? @constant.builtin "^__(FILE|LINE|ENCODING)__$")) diff --git a/languages/yaml-erb/injections.scm b/languages/yaml-erb/injections.scm index 1475e8b..c651c7c 100644 --- a/languages/yaml-erb/injections.scm +++ b/languages/yaml-erb/injections.scm @@ -7,5 +7,10 @@ (#set! "combined")) ((comment) @content + (#match? @content "^\\s*locals:\\s+\\(") + (#set! "language" "ruby")) + +((comment) @content + (#not-match? @content "^\\s*locals:\\s+\\(") (#set! injection.language "comment") (#set! "combined")) diff --git a/tests/erb.rs b/tests/erb.rs index f712183..1e4ef25 100644 --- a/tests/erb.rs +++ b/tests/erb.rs @@ -8,3 +8,42 @@ fn injections() { "languages/erb/injections.scm", ); } + +#[test] +fn strict_locals_are_injected_as_ruby() { + let source = "<%# locals: (title:, subtitle: nil, **options) %>"; + let query = std::fs::read_to_string("languages/erb/injections.scm").unwrap(); + let captures = support::run_query(source, &query, "erb"); + + assert_eq!(captures.len(), 1); + assert_eq!(captures[0].name, "content"); + assert_eq!( + captures[0].text, + " locals: (title:, subtitle: nil, **options) " + ); +} + +#[test] +fn strict_locals_require_whitespace_before_parameters() { + let source = "<%# locals:(title:) %>"; + let query = std::fs::read_to_string("languages/erb/injections.scm").unwrap(); + let captures = support::run_query(source, &query, "erb"); + + assert_eq!(captures.len(), 1); + assert_eq!(captures[0].name, "content"); + assert_eq!(captures[0].text, " locals:(title:) "); +} + +#[test] +fn strict_local_names_share_parameter_highlighting() { + let source = "locals: (title:, talks:, view_all_path: root_path, subtitle: nil)"; + let query = std::fs::read_to_string("languages/ruby/highlights.scm").unwrap(); + let captures = support::run_query(source, &query, "ruby"); + let parameters: Vec<_> = captures + .iter() + .filter(|capture| capture.name == "variable.parameter.keyword") + .map(|capture| capture.text.as_str()) + .collect(); + + assert_eq!(parameters, ["title", "talks", "view_all_path", "subtitle"]); +} diff --git a/tests/languages/erb/injections.erb b/tests/languages/erb/injections.erb index 7584915..cb9b5d2 100644 --- a/tests/languages/erb/injections.erb +++ b/tests/languages/erb/injections.erb @@ -2,6 +2,8 @@ Hello <%= user.name %> <%# Embedded template comment %> +<%# locals: (title:, subtitle: nil, **options) %> + <% if admin? %> Welcome, <%= admin_name %> <% end %> diff --git a/tests/languages/erb/snapshots/injections.snap b/tests/languages/erb/snapshots/injections.snap index bd9e9b3..85c0dc2 100644 --- a/tests/languages/erb/snapshots/injections.snap +++ b/tests/languages/erb/snapshots/injections.snap @@ -24,25 +24,33 @@ expression: captures text: "\n\n" - name: content line: 5 + column: 4 + text: " locals: (title:, subtitle: nil, **options) " +- name: content + line: 5 + column: 50 + text: "\n\n" +- name: content + line: 7 column: 3 text: " if admin? " - name: content - line: 5 + line: 7 column: 16 text: "\n Welcome, " - name: content - line: 6 + line: 8 column: 15 text: " admin_name " - name: content - line: 6 + line: 8 column: 29 text: "\n" - name: content - line: 7 + line: 9 column: 3 text: " end " - name: content - line: 7 + line: 9 column: 10 text: "\n"