Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,40 @@ All notable changes to this project will be documented in this file.

This project follows [Semantic Versioning](https://semver.org/).

## [0.3.0] - 2026-03-26

### Added

- **575 tests** across 7 suites (up from 105):
- 247 indentation tests covering all 32 sections of the indent spec
- 139 syntax highlighting tests for all token types
- 39 grammar accuracy and performance tests
- 25 code folding tests
- 20 autocompletion tests (10 with documented skips)
- 10 full-file integration tests
- Parse accuracy benchmark script (`test/benchmark.mjs`)

### Fixed

- **35 grammar bugs identified, 29 fixed** across 8 root causes:
- Bare `rescue` inside method body without `begin`
- Lambda without params (`-> { body }`)
- Setter assignment via dot (`self.email = value`)
- Constants as method calls (`URI(url)`, `Integer("42")`)
- Scope resolution assignment (`Foo::BAR = 1`)
- Block param destructuring (`|(k, v), acc|`)
- `super` with bare arguments
- Proc call syntax (`proc_var.(args)`)
- Backslash line continuation
- Pattern binding in `case/in`
- `private def`, `protected def`, `private_class_method def` now correctly recognized as block openers for indentation
- Mixed `do/end` + `{}` block nesting no longer confuses the deindent scanner
- `super` highlighted as keyword

### Changed

- Parse accuracy improved significantly (85-91% → 93-98%) across all benchmark files

## [0.2.0] - 2026-03-26

### Added
Expand Down
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Ruby language support for [CodeMirror 6](https://codemirror.net/), built on a [Lezer](https://lezer.codemirror.net/) grammar.

Targets **Ruby 3.0+** syntax (including endless methods and basic pattern matching).

[**Live Demo**](https://jeanpaulsio.github.io/codemirror-lang-ruby/)

## Install
Expand Down Expand Up @@ -29,13 +31,13 @@ Tested against popular open source Ruby projects (large, representative files):

| Project | File | Lines | Accuracy |
|---------|------|-------|----------|
| [Fastlane](https://github.com/fastlane/fastlane) | runner.rb | 379 | **91.3%** |
| [Grape](https://github.com/ruby-grape/grape) | api.rb | 166 | **90.1%** |
| [Jekyll](https://github.com/jekyll/jekyll) | site.rb | 577 | **88.4%** |
| [Devise](https://github.com/heartcombo/devise) | devise.rb | 534 | **87.9%** |
| [Sidekiq](https://github.com/sidekiq/sidekiq) | config.rb | 321 | **87.5%** |
| [Rails](https://github.com/rails/rails) | query_methods.rb | 2291 | **86.6%** |
| [Faker](https://github.com/faker-ruby/faker) | internet.rb | 579 | **85.3%** |
| [Faker](https://github.com/faker-ruby/faker) | internet.rb | 579 | **98.6%** |
| [Devise](https://github.com/heartcombo/devise) | devise.rb | 534 | **98.1%** |
| [Jekyll](https://github.com/jekyll/jekyll) | site.rb | 577 | **97.6%** |
| [Fastlane](https://github.com/fastlane/fastlane) | runner.rb | 379 | **95.0%** |
| [Rails](https://github.com/rails/rails) | query_methods.rb | 2291 | **94.2%** |
| [Grape](https://github.com/ruby-grape/grape) | api.rb | 166 | **94.0%** |
| [Sidekiq](https://github.com/sidekiq/sidekiq) | config.rb | 321 | **93.5%** |

## What's supported

Expand All @@ -61,13 +63,14 @@ Tested against popular open source Ruby projects (large, representative files):
- **Inline rescue as a standalone expression** — `value = foo rescue nil` works (rescue in assignments), but `foo rescue bar` as a standalone expression outside of assignment context is not supported.
- **Newline as statement separator** — Ruby uses newlines to separate statements, but the grammar is whitespace-insensitive. An expression followed by `if` on the next line may be parsed as a conditional modifier (e.g., `x = 1\nif cond` parses as `x = (1 if cond)`).
- **Symbol-key shorthand in method calls** — `foo(name: "value")` with symbol-key syntax is not yet supported. Use rocket syntax `foo(:name => "value")` as a workaround.
- **Line-based indentation** — The indent engine uses regex line scanning rather than tree-based analysis. Most patterns work well, but complex multi-line expressions (e.g., multi-line method args followed by a body, trailing commas inside nested delimiters) may not indent perfectly.

## Development

```bash
npm install # Install dependencies
npm run build # Build grammar + bundle to dist/
npm test # Run all 105 grammar tests
npm test # Run 105 grammar tests (575 total across all suites)
npm run lint # TypeScript type check
npm run demo:build # Build the demo page
```
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codemirror-lang-ruby",
"version": "0.2.0",
"version": "0.3.0",
"description": "Ruby language support for CodeMirror 6, built on a Lezer grammar",
"type": "module",
"main": "dist/index.cjs",
Expand Down
102 changes: 102 additions & 0 deletions test/benchmark.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Parse accuracy benchmark against real-world Ruby projects
// Downloads files from GitHub and measures error-free line percentage

import {rubyLanguage} from "../dist/index.js"
const parser = rubyLanguage.parser

const FILES = [
{
project: "Fastlane",
file: "runner.rb",
url: "https://raw.githubusercontent.com/fastlane/fastlane/master/fastlane/lib/fastlane/runner.rb",
},
{
project: "Grape",
file: "api.rb",
url: "https://raw.githubusercontent.com/ruby-grape/grape/master/lib/grape/api.rb",
},
{
project: "Jekyll",
file: "site.rb",
url: "https://raw.githubusercontent.com/jekyll/jekyll/master/lib/jekyll/site.rb",
},
{
project: "Devise",
file: "devise.rb",
url: "https://raw.githubusercontent.com/heartcombo/devise/main/lib/devise.rb",
},
{
project: "Sidekiq",
file: "config.rb",
url: "https://raw.githubusercontent.com/sidekiq/sidekiq/main/lib/sidekiq/config.rb",
},
{
project: "Rails",
file: "query_methods.rb",
url: "https://raw.githubusercontent.com/rails/rails/main/activerecord/lib/active_record/relation/query_methods.rb",
},
{
project: "Faker",
file: "internet.rb",
url: "https://raw.githubusercontent.com/faker-ruby/faker/main/lib/faker/default/internet.rb",
},
]

async function fetchFile(url) {
const res = await fetch(url)
if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status}`)
return await res.text()
}

function measureAccuracy(code) {
const tree = parser.parse(code)
const lines = code.split("\n")
const totalLines = lines.length

// Find which lines contain error nodes
const errorLines = new Set()
tree.iterate({
enter(node) {
if (node.type.isError) {
// Mark all lines this error spans
const startLine = code.slice(0, node.from).split("\n").length
const endLine = code.slice(0, node.to).split("\n").length
for (let i = startLine; i <= endLine; i++) {
errorLines.add(i)
}
}
},
})

const cleanLines = totalLines - errorLines.size
const accuracy = (cleanLines / totalLines) * 100
return { totalLines, errorLines: errorLines.size, cleanLines, accuracy }
}

console.log("Fetching files and measuring parse accuracy...\n")

const results = []
for (const entry of FILES) {
try {
const code = await fetchFile(entry.url)
const stats = measureAccuracy(code)
results.push({ ...entry, ...stats })
console.log(
`${entry.project.padEnd(12)} ${entry.file.padEnd(22)} ${String(stats.totalLines).padStart(5)} lines ${stats.accuracy.toFixed(1)}%`
)
} catch (e) {
console.log(`${entry.project.padEnd(12)} FAILED: ${e.message}`)
}
}

// Sort by accuracy descending
results.sort((a, b) => b.accuracy - a.accuracy)

console.log("\n--- README table (sorted by accuracy) ---\n")
console.log("| Project | File | Lines | Accuracy |")
console.log("|---------|------|-------|----------|")
for (const r of results) {
console.log(
`| ${r.project} | ${r.file} | ${r.totalLines} | **${r.accuracy.toFixed(1)}%** |`
)
}
Loading