From 4f1f4de5d70687b516123386352400c9928e4aa1 Mon Sep 17 00:00:00 2001 From: Toshimaru Date: Thu, 30 Apr 2026 19:11:28 +0900 Subject: [PATCH 1/4] Add Ruby syntax highlighting --- crates/lsh/definitions/ruby.lsh | 69 +++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 crates/lsh/definitions/ruby.lsh diff --git a/crates/lsh/definitions/ruby.lsh b/crates/lsh/definitions/ruby.lsh new file mode 100644 index 00000000000..9d10711944c --- /dev/null +++ b/crates/lsh/definitions/ruby.lsh @@ -0,0 +1,69 @@ +#[display_name = "Ruby"] +#[path = "**/*.rb"] +#[path = "**/*.rake"] +#[path = "**/*.ru"] +#[path = "**/Gemfile"] +#[path = "**/Rakefile"] +pub fn ruby() { + until /$/ { + yield other; + + if /#.*/ { + yield comment; + } else if /=begin\>/ { + loop { + yield comment; + await input; + if /=end\>/ { + yield comment; + break; + } + } + } else if /'/ { + until /$/ { + yield string; + if /\\./ {} + else if /'/ { yield string; break; } + await input; + } + } else if /"/ { + until /$/ { + yield string; + if /\\./ {} + else if /"/ { yield string; break; } + await input; + } + } else if /`/ { + loop { + yield string; + if /\\./ {} + else if /`/ { yield string; break; } + await input; + } + } else if /(?:BEGIN|END|alias|begin|break|case|do|else|elsif|ensure|for|if|in|next|redo|rescue|retry|return|then|unless|until|when|while|yield)\>/ { + yield keyword.control; + } else if /(?:class|def|defined\?|end|module|self|super|undef)\>/ { + yield keyword.other; + } else if /(?:true|false|nil)\>/ { + yield constant.language; + } else if /(?i:-?(?:0x[\da-fA-F_]+|0b[01_]+|0o[0-7_]+|[\d_]+\.?[\d_]*|\.[\d_]+)(?:e[+-]?[\d_]+)?r?i?)/ { + if /\w+/ { + // Invalid numeric literal + } else { + yield constant.numeric; + } + } else if /[@$]+[A-Za-z_]\w*[!?=]?/ { + yield variable; + } else if /:[A-Za-z_]\w*[!?=]?/ { + yield constant.language; + } else if /[A-Z]\w*/ { + yield constant.language; + } else if /(\w+[!?=]?)\s*\(/ { + yield $1 as method; + } else if /\w+[!?=]?/ { + // Gobble word chars to align the next iteration on a word boundary. + } + + yield other; + } +} From f34c93dd53b67c56203fea3e2f71bd8d6b5ea596 Mon Sep 17 00:00:00 2001 From: Toshimaru Date: Thu, 30 Apr 2026 19:18:56 +0900 Subject: [PATCH 2/4] Add Ruby syntax-highlighting test fixture. - Fix Ruby keyword and variable highlighting --- assets/highlighting-tests/ruby.rb | 104 ++++++++++++++++++++++++++++++ crates/lsh/definitions/ruby.lsh | 31 +++++++-- 2 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 assets/highlighting-tests/ruby.rb diff --git a/assets/highlighting-tests/ruby.rb b/assets/highlighting-tests/ruby.rb new file mode 100644 index 00000000000..a8d2222e137 --- /dev/null +++ b/assets/highlighting-tests/ruby.rb @@ -0,0 +1,104 @@ +# Comments +# Single-line comment + +=begin +Multi-line +comment +=end + +# Numbers +42 +3.14 +0.5 +1e10 +1.5e-3 +0xff +0xFF +0b1010 +0o77 +1_000_000 +3.14r +2i + +# Constants +true +false +nil +Object +String + +# Strings +'single quotes with escape: \' \n \t \\' +"double quotes with escape: \" \n \t \\" +`echo shell command` + +# Symbols and variables +:symbol +:method_name? +@instance_var +@@class_var +$global_var + +# Control flow keywords +if true + puts "yes" +elsif false + puts "no" +else + puts "maybe" +end + +unless nil + puts "not nil" +end + +case 42 +when 1 + puts "one" +else + puts "other" +end + +for i in 1..3 + next if i == 2 + break if i == 3 +end + +while false + redo +end + +begin + raise "oops" +rescue StandardError => e + retry +ensure + puts e +end + +# Definitions and method calls +module Demo + class Animal + def initialize(name) + @name = name + end + + def speak! + puts "#{@name} speaks" + end + end +end + +alias old_speak speak! +undef old_speak + +BEGIN { puts "start" } +END { puts "finish" } +defined? Demo +self +super +yield + +puts "hello" +Array.new(3) +greet("world") diff --git a/crates/lsh/definitions/ruby.lsh b/crates/lsh/definitions/ruby.lsh index 9d10711944c..a46b99b38a1 100644 --- a/crates/lsh/definitions/ruby.lsh +++ b/crates/lsh/definitions/ruby.lsh @@ -40,19 +40,42 @@ pub fn ruby() { else if /`/ { yield string; break; } await input; } - } else if /(?:BEGIN|END|alias|begin|break|case|do|else|elsif|ensure|for|if|in|next|redo|rescue|retry|return|then|unless|until|when|while|yield)\>/ { + } else if /(?:begin|break|case|do|else|elsif|ensure|for|if|in|next|redo|rescue|retry|return|then|unless|until|when|while)\>/ { yield keyword.control; - } else if /(?:class|def|defined\?|end|module|self|super|undef)\>/ { + } else if /def\s+(\w+[!?=]?)/ { + yield keyword.other; + yield $1 as method; + } else if /(?:BEGIN|END|alias|class|defined\?|def|end|module|self|super|undef|yield)\>/ { yield keyword.other; } else if /(?:true|false|nil)\>/ { yield constant.language; - } else if /(?i:-?(?:0x[\da-fA-F_]+|0b[01_]+|0o[0-7_]+|[\d_]+\.?[\d_]*|\.[\d_]+)(?:e[+-]?[\d_]+)?r?i?)/ { + } else if /\.\.\.?/ { + // Range operator. + } else if /(?i:-?(?:0x[\da-fA-F_]+|0b[01_]+|0o[0-7_]+)r?i?)/ { + if /\w+/ { + // Invalid numeric literal + } else { + yield constant.numeric; + } + } else if /(?i:-?[\d_]+\.[\d_]+(?:e[+-]?[\d_]+)?r?i?)/ { + if /\w+/ { + // Invalid numeric literal + } else { + yield constant.numeric; + } + } else if /(?i:-?\.[\d_]+(?:e[+-]?[\d_]+)?r?i?)/ { + if /\w+/ { + // Invalid numeric literal + } else { + yield constant.numeric; + } + } else if /(?i:-?[\d_]+(?:e[+-]?[\d_]+)?r?i?)/ { if /\w+/ { // Invalid numeric literal } else { yield constant.numeric; } - } else if /[@$]+[A-Za-z_]\w*[!?=]?/ { + } else if /(?:@{1,2}|\$)[A-Za-z_]\w*/ { yield variable; } else if /:[A-Za-z_]\w*[!?=]?/ { yield constant.language; From 07cbc0dab543fec9e3cab3465cede94e3320bee5 Mon Sep 17 00:00:00 2001 From: Toshimaru Date: Mon, 4 May 2026 20:43:12 +0900 Subject: [PATCH 3/4] fix: Guard =begin/=end block comments at column 0 - use `keyword.control` for `end` --- assets/highlighting-tests/ruby.rb | 4 +--- crates/lsh/definitions/ruby.lsh | 30 +++++++++++++++++++----------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/assets/highlighting-tests/ruby.rb b/assets/highlighting-tests/ruby.rb index a8d2222e137..69b1c682316 100644 --- a/assets/highlighting-tests/ruby.rb +++ b/assets/highlighting-tests/ruby.rb @@ -83,9 +83,7 @@ def initialize(name) @name = name end - def speak! - puts "#{@name} speaks" - end + def speak! = puts "#{@name} speaks" end end diff --git a/crates/lsh/definitions/ruby.lsh b/crates/lsh/definitions/ruby.lsh index a46b99b38a1..0e7079cd12d 100644 --- a/crates/lsh/definitions/ruby.lsh +++ b/crates/lsh/definitions/ruby.lsh @@ -5,20 +5,28 @@ #[path = "**/Gemfile"] #[path = "**/Rakefile"] pub fn ruby() { + var zero = 0; until /$/ { yield other; - if /#.*/ { - yield comment; - } else if /=begin\>/ { - loop { - yield comment; - await input; - if /=end\>/ { + if off == zero { + if /=begin\>/ { + loop { yield comment; - break; + await input; + if off == zero { + if /=end\>/ { + yield comment; + break; + } + } } + continue; } + } + + if /#.*/ { + yield comment; } else if /'/ { until /$/ { yield string; @@ -40,12 +48,12 @@ pub fn ruby() { else if /`/ { yield string; break; } await input; } - } else if /(?:begin|break|case|do|else|elsif|ensure|for|if|in|next|redo|rescue|retry|return|then|unless|until|when|while)\>/ { + } else if /(?:begin|break|case|class|do|else|elsif|end|ensure|for|if|in|module|next|redo|rescue|retry|return|then|unless|until|when|while)\>/ { yield keyword.control; } else if /def\s+(\w+[!?=]?)/ { - yield keyword.other; + yield keyword.control; yield $1 as method; - } else if /(?:BEGIN|END|alias|class|defined\?|def|end|module|self|super|undef|yield)\>/ { + } else if /(?:BEGIN|END|alias|defined\?|self|super|undef|yield)\>/ { yield keyword.other; } else if /(?:true|false|nil)\>/ { yield constant.language; From 61c10323206cabd3abeabd52d7b4b6aca1007fb2 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 30 Jul 2026 00:08:47 +0200 Subject: [PATCH 4/4] Simplify LSH --- assets/highlighting-tests/ruby.rb | 39 +++++++++++-- crates/lsh/definitions/ruby.lsh | 97 ++++++++++++------------------- 2 files changed, 72 insertions(+), 64 deletions(-) diff --git a/assets/highlighting-tests/ruby.rb b/assets/highlighting-tests/ruby.rb index 69b1c682316..d8e4cc25af2 100644 --- a/assets/highlighting-tests/ruby.rb +++ b/assets/highlighting-tests/ruby.rb @@ -1,9 +1,11 @@ -# Comments # Single-line comment =begin -Multi-line -comment +Multi-line comment +=end + +=begin rdoc +The rest of the `=begin` line is ignored by Ruby. =end # Numbers @@ -19,6 +21,9 @@ 1_000_000 3.14r 2i +1.upto(3) +(1..3).to_a +(1...3).to_a # Constants true @@ -26,18 +31,24 @@ nil Object String +Demo::Animal +::Kernel +MAX_SIZE = 10 # Strings 'single quotes with escape: \' \n \t \\' "double quotes with escape: \" \n \t \\" +"interpolated: #{1 + 1} and a # that is not a comment" `echo shell command` # Symbols and variables :symbol :method_name? +attr_accessor :name, :age @instance_var @@class_var $global_var +{ key: 'value' } # Control flow keywords if true @@ -68,6 +79,10 @@ redo end +until true + puts "spin" +end + begin raise "oops" rescue StandardError => e @@ -76,6 +91,9 @@ puts e end +puts "even" if 42.even? and not false +puts "odd" or true + # Definitions and method calls module Demo class Animal @@ -83,7 +101,19 @@ def initialize(name) @name = name end - def speak! = puts "#{@name} speaks" + def self.create(name) + new(name) + end + + def ==(other) + @name == other.name + end + + def name=(value) + @name = value + end + + def speak! = puts "#{@name} speaks" end end @@ -93,6 +123,7 @@ def speak! = puts "#{@name} speaks" BEGIN { puts "start" } END { puts "finish" } defined? Demo +puts __FILE__, __LINE__ self super yield diff --git a/crates/lsh/definitions/ruby.lsh b/crates/lsh/definitions/ruby.lsh index 0e7079cd12d..ddc3973f01c 100644 --- a/crates/lsh/definitions/ruby.lsh +++ b/crates/lsh/definitions/ruby.lsh @@ -1,83 +1,58 @@ #[display_name = "Ruby"] -#[path = "**/*.rb"] +#[path = "**/*.gemspec"] #[path = "**/*.rake"] +#[path = "**/*.rb"] #[path = "**/*.ru"] #[path = "**/Gemfile"] #[path = "**/Rakefile"] pub fn ruby() { - var zero = 0; - until /$/ { - yield other; - - if off == zero { - if /=begin\>/ { - loop { - yield comment; - await input; - if off == zero { - if /=end\>/ { - yield comment; - break; - } - } - } - continue; + if /=begin\>.*/ { + loop { + yield comment; + await input; + if /=end\>.*/ { + yield comment; + break; } + if /.*/ {} } + return; + } + + until /$/ { + yield other; if /#.*/ { yield comment; } else if /'/ { - until /$/ { - yield string; - if /\\./ {} - else if /'/ { yield string; break; } - await input; - } + single_quote_string(); } else if /"/ { - until /$/ { - yield string; - if /\\./ {} - else if /"/ { yield string; break; } - await input; - } + double_quote_string(); } else if /`/ { - loop { - yield string; - if /\\./ {} - else if /`/ { yield string; break; } - await input; + until /$/ { + if /\\./ { + // Escape sequences + } else if /`/ { + yield string; + break; + } } - } else if /(?:begin|break|case|class|do|else|elsif|end|ensure|for|if|in|module|next|redo|rescue|retry|return|then|unless|until|when|while)\>/ { + } else if /(?:begin|break|case|do|elsif|else|end|ensure|for|if|in|next|redo|rescue|retry|return|then|unless|until|when|while)\>/ { yield keyword.control; + } else if /def\s+\w+\.(\w+[!?=]?)/ { + // Singleton method, as in `def self.foo`. + yield keyword.other; + yield $1 as method; } else if /def\s+(\w+[!?=]?)/ { - yield keyword.control; + yield keyword.other; yield $1 as method; - } else if /(?:BEGIN|END|alias|defined\?|self|super|undef|yield)\>/ { + } else if /(?:BEGIN|END|__ENCODING__|__FILE__|__LINE__|alias|and|class|defined\?|def|module|not|or|self|super|undef|yield)\>/ { yield keyword.other; } else if /(?:true|false|nil)\>/ { yield constant.language; - } else if /\.\.\.?/ { - // Range operator. - } else if /(?i:-?(?:0x[\da-fA-F_]+|0b[01_]+|0o[0-7_]+)r?i?)/ { - if /\w+/ { - // Invalid numeric literal - } else { - yield constant.numeric; - } - } else if /(?i:-?[\d_]+\.[\d_]+(?:e[+-]?[\d_]+)?r?i?)/ { - if /\w+/ { - // Invalid numeric literal - } else { - yield constant.numeric; - } - } else if /(?i:-?\.[\d_]+(?:e[+-]?[\d_]+)?r?i?)/ { - if /\w+/ { - // Invalid numeric literal - } else { - yield constant.numeric; - } - } else if /(?i:-?[\d_]+(?:e[+-]?[\d_]+)?r?i?)/ { + } else if /(?i:-?(?:0x[\da-f_]+|0b[01_]+|0o[0-7_]+|[\d_]+\.[\d_]+(?:e[+-]?[\d_]+)?|[\d_]+(?:e[+-]?[\d_]+)?)r?i?)/ { + // Floats need digits on both sides of the dot, so that `1..2` and + // `1.upto(2)` don't swallow the dot. if /\w+/ { // Invalid numeric literal } else { @@ -85,10 +60,12 @@ pub fn ruby() { } } else if /(?:@{1,2}|\$)[A-Za-z_]\w*/ { yield variable; + } else if /::/ { + // Scope resolution, so that `Foo::Bar` isn't mistaken for a symbol. } else if /:[A-Za-z_]\w*[!?=]?/ { yield constant.language; } else if /[A-Z]\w*/ { - yield constant.language; + yield storage.type; } else if /(\w+[!?=]?)\s*\(/ { yield $1 as method; } else if /\w+[!?=]?/ {