Skip to content

Commit c1efd3b

Browse files
authored
feat: subscript access, setter methods, def self.method, heredoc fix (#21)
* feat: add subscript access, setter methods, def self.method, heredoc fix Subscript expressions: - arr[0], hash[:key], matrix[i][j] via SubscriptExpression - Subscript assignment: arr[0] = 42, HASH[key] ||= [] Method definition improvements: - Setter methods: def name=(val) ... end - Class methods: def self.method(args) ... end (with and without parens) Heredoc vs left-shift fix: - Bare identifier heredocs (<<FILE) now reject non-whitespace after the delimiter on the opening line, preventing <<File.expand_path() from being misread as a heredoc Real-world accuracy improvement: - Devise: 98.0% → 99.1% - All 7 repos above 95% 88 tests passing (5 new). * fix: improve indentation deindent on end/else/elsif/rescue keywords Indentation rules now check context.textAfter for closing keywords. When typing `end`, `else`, `elsif`, `when`, `rescue`, `ensure` on a new line inside a block, the line deindents to align with the opening keyword (def, class, if, begin, etc.). Previously the indent functions always added one unit regardless of what was being typed, so `end` would stay indented until manually adjusted. * feat: restore hash symbol-key shorthand and fix indentation Hash shorthand: - {name: "Alice", age: 30} works again - Moved Symbol from inline token to external tokenizer (symbolTokenizer) to eliminate : vs Symbol overlap with Block in expressions - colonOp handles : in ternary and hash shorthand contexts - Same pattern as /, <, % external tokenizers Indentation fix: - Indent functions now check context.textAfter for closing keywords - Typing `end`, `else`, `elsif`, `when`, `rescue`, `ensure` immediately deindents to match the opening keyword - After `end`, pressing enter starts at column 0 (not indented) - Intermediate keywords (elsif, else, rescue) indent their bodies 89 tests passing. * fix: rebuild indentation using indentService for incomplete code The previous approach used indentNodeProp which depends on complete parse tree nodes (MethodBody, ClassBody, etc.). These don't exist while typing because the code is incomplete (no `end` yet). New approach: text-based indentService that runs BEFORE tree-based indentation. It checks the previous line's content: - After def/class/module/if/while/etc. → indent one level - After else/elsif/rescue/ensure/when → indent one level - After end → stay at end's level - On end/else/elsif/rescue/ensure → deindent to match opener - Default → maintain previous line's indentation Tree-based indentation (indentNodeProp) is kept only for bracket delimited constructs ([], {}, ()) where delimitedIndent works well. 89 tests passing. * fix: handle single-line forms and add comprehensive indent tests Single-line forms like `class Foo; end`, `def foo; body; end`, and endless methods `def foo(x) = x + 1` no longer incorrectly indent the next line. Added 76 programmatic indentation tests covering block openers, mid-block keywords, end alignment, nesting, modifier forms, and real-world patterns. Also adds Tab key support and Cmd+Shift+Enter for insert-line-above in the demo editor. * feat: comprehensive indentation covering all 18 spec sections Rewrote indent service to handle: - Closing delimiters (}, ], )) via backward bracket scanning - Continuation lines (trailing +, -, *, &&, ||, \, comma) - Method chaining with leading dots (.bar, .baz) - Assignment with block openers (x = if, x = case, @foo ||= begin) - Chain-end detection: returns to base indent when chain/continuation ends - Lambda/Proc blocks (-> (x) {, -> (x) do) - Multi-line method def arguments 119 programmatic indent tests covering all 18 sections from the spec: sections 1-8 (block openers, mid-block, end, delimiters, nesting, modifiers, single-line), 9 (heredocs), 10 (continuations + chaining), 11 (lambdas), 12 (strings), 13 (assignment blocks), 14 (real-world), 15 (edge cases), 16 (when/in style), 17 (multi-line args), 18 (conditional assign). * fix: Cmd+Shift+Enter inserts line with correct indentation The insert-line-above keybinding now calculates the proper indent level for the new line instead of always placing the cursor at column 0. Also rebuilt demo bundle with latest indentation fixes. * docs: update README with current test counts and remove fixed limitations * chore: bump to 0.2.0, update accuracy benchmarks Updated README with honest parse accuracy numbers from larger real-world files (87.4% overall). Documented known limitations from error pattern analysis. Bumped version to 0.2.0 for the comprehensive indentation overhaul and edge case fixes. * docs: add CHANGELOG for 0.1.0 and 0.2.0
1 parent 0342db2 commit c1efd3b

14 files changed

Lines changed: 923 additions & 98 deletions

CHANGELOG.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
This project follows [Semantic Versioning](https://semver.org/).
6+
7+
## [0.2.0] - 2026-03-26
8+
9+
### Added
10+
11+
- **Comprehensive indentation** — 119 programmatic tests covering all Ruby indent/dedent patterns:
12+
- Block openers (def, class, module, if, unless, while, until, for, case, begin, do, braces)
13+
- Mid-block keywords (else, elsif, when, rescue, ensure)
14+
- Closing delimiters (}, ], ))
15+
- Continuation lines (trailing +, -, &&, ||, \, comma)
16+
- Method chaining with leading dots
17+
- Assignment with block openers (`x = if`, `@foo ||= begin`)
18+
- Modifier forms and single-line bodies (no indent change)
19+
- Nested blocks at arbitrary depth
20+
- **Subscript expressions**`arr[0]`, `hash[:key]`, `matrix[i][j]`
21+
- **Subscript assignment**`hash[:key] = value`
22+
- **Setter method definitions**`def name=(value)`
23+
- **Class method definitions**`def self.method_name`
24+
- Tab key support in demo editor
25+
- Cmd+Shift+Enter inserts properly indented line above in demo editor
26+
27+
### Fixed
28+
29+
- Single-line forms (`class Foo; end`, `def foo; end`, endless methods) no longer incorrectly indent the next line
30+
- `end`, `else`, `elsif`, `when`, `rescue`, `ensure` correctly deindent to match their opening keyword at any nesting depth
31+
- Empty lines inside blocks preserve surrounding indent context
32+
- Closing delimiters align with their matching opener
33+
34+
### Changed
35+
36+
- Indentation engine rewritten from tree-based `indentNodeProp` to text-based `indentService` for robust handling of incomplete code while typing
37+
- Updated parse accuracy benchmarks with larger, more representative real-world files
38+
39+
## [0.1.0] - 2026-03-26
40+
41+
### Added
42+
43+
- Initial release
44+
- Lezer grammar for Ruby with 89 grammar tests
45+
- **Definitions**: methods (with params, endless `def f(x) = expr`), classes (with inheritance), modules
46+
- **Control flow**: if/elsif/else, unless, while, until, for/in, case/when, case/in (pattern matching)
47+
- **Error handling**: begin/rescue/ensure/raise
48+
- **Strings**: single-quoted, double-quoted with `#{interpolation}`, heredocs (`<<~DELIM`), `%`-literals
49+
- **Literals**: integers, floats, symbols, character literals, arrays, hashes, regex, nil, true, false
50+
- **Expressions**: assignment (including `||=`, `&&=`), multiple assignment, method calls, chained calls, binary/unary/ternary operators, lambdas, ranges, conditional modifiers
51+
- **Blocks**: brace blocks and do/end blocks attached to method calls
52+
- **Operators**: proper precedence, safe navigation (`&.`), scope resolution (`::`)
53+
- **Bare method calls**: 25 common Ruby methods (puts, require, attr_reader, include, etc.)
54+
- **Variables**: local, @instance, @@class, $global, Constants
55+
- **Comments**: line `#` and block `=begin`/`=end`
56+
- 5 external tokenizers for context-dependent tokens (regex/division, heredoc/less-than, percent-literal/modulo, symbol/colon, string interpolation)
57+
- Code folding, bracket closing, keyword autocompletion (31 keywords)
58+
- Demo page with One Dark theme
59+
- GitHub Actions CI

README.md

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,17 @@ new EditorView({
2525

2626
## Real-world parse accuracy
2727

28-
Tested against popular open source Ruby projects:
29-
30-
| Project | Stars | Lines | Accuracy |
31-
|---------|-------|-------|----------|
32-
| [Rails](https://github.com/rails/rails) | 56k | 338 | **99.7%** |
33-
| [Jekyll](https://github.com/jekyll/jekyll) | 49k | 196 | **99.6%** |
34-
| [Fastlane](https://github.com/fastlane/fastlane) | 40k | 54 | **99.3%** |
35-
| [Devise](https://github.com/heartcombo/devise) | 24k | 534 | **98.0%** |
36-
| [Faker](https://github.com/faker-ruby/faker) | 11k | 282 | **97.5%** |
37-
| [Sidekiq](https://github.com/sidekiq/sidekiq) | 13k | 162 | **97.5%** |
38-
| [Grape](https://github.com/ruby-grape/grape) | 10k | 75 | **95.3%** |
28+
Tested against popular open source Ruby projects (large, representative files):
29+
30+
| Project | File | Lines | Accuracy |
31+
|---------|------|-------|----------|
32+
| [Fastlane](https://github.com/fastlane/fastlane) | runner.rb | 379 | **91.3%** |
33+
| [Grape](https://github.com/ruby-grape/grape) | api.rb | 166 | **90.1%** |
34+
| [Jekyll](https://github.com/jekyll/jekyll) | site.rb | 577 | **88.4%** |
35+
| [Devise](https://github.com/heartcombo/devise) | devise.rb | 534 | **87.9%** |
36+
| [Sidekiq](https://github.com/sidekiq/sidekiq) | config.rb | 321 | **87.5%** |
37+
| [Rails](https://github.com/rails/rails) | query_methods.rb | 2291 | **86.6%** |
38+
| [Faker](https://github.com/faker-ruby/faker) | internet.rb | 579 | **85.3%** |
3939

4040
## What's supported
4141

@@ -50,21 +50,22 @@ Tested against popular open source Ruby projects:
5050
- **Bare method calls**: `puts "hello"`, `require "json"`, `attr_reader :name`, `include Comparable` (25 common Ruby methods)
5151
- **Variables**: local, `@instance`, `@@class`, `$global`, `Constants`
5252
- **Comments**: line `#` and block `=begin`/`=end`
53-
- **Editor features**: indentation, code folding, bracket closing, keyword autocompletion (31 keywords)
53+
- **Editor features**: smart indentation (119 test cases), code folding, bracket closing, keyword autocompletion (31 keywords)
5454

5555
## Known limitations
5656

57+
- Blank lines inside class/method bodies can produce spurious error nodes (~38% of parse errors)
58+
- Multi-arg bare method calls (`raise ArgumentError, "msg"`) not fully supported
59+
- Compound assignment operators (`+=`, `-=`, `|=`) in some contexts
5760
- Heredoc and `%`-literal bodies are opaque tokens (no interpolation highlighting inside)
58-
- Hash symbol-key shorthand (`{ name: "Alice" }`) not supported (`:` conflicts with Symbol token when blocks are expressions)
59-
- Setter method definitions (`def name=(val)`) and subscript access (`arr[0]`) not yet supported
6061
- `<<` left-shift operator not distinguished from heredoc start
6162

6263
## Development
6364

6465
```bash
6566
npm install # Install dependencies
6667
npm run build # Build grammar + bundle to dist/
67-
npm test # Run all 83 grammar tests
68+
npm test # Run all 89 grammar tests
6869
npm run lint # TypeScript type check
6970
npm run demo:build # Build the demo page
7071
```

demo/demo.js

Lines changed: 252 additions & 45 deletions
Large diffs are not rendered by default.

demo/demo.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import {EditorView, basicSetup} from "codemirror"
2+
import {keymap} from "@codemirror/view"
3+
import {indentWithTab} from "@codemirror/commands"
4+
import {getIndentation} from "@codemirror/language"
25
import {oneDark} from "@codemirror/theme-one-dark"
36
import {ruby} from "../src/index"
47

@@ -73,6 +76,16 @@ new EditorView({
7376
doc: sampleCode,
7477
extensions: [
7578
basicSetup,
79+
keymap.of([
80+
indentWithTab,
81+
{key: "Mod-Shift-Enter", run: (view) => {
82+
const line = view.state.doc.lineAt(view.state.selection.main.head)
83+
const indent = getIndentation(view.state, line.from)
84+
const pad = indent != null && indent > 0 ? " ".repeat(indent) : ""
85+
view.dispatch({changes: {from: line.from, insert: pad + "\n"}, selection: {anchor: line.from + pad.length}})
86+
return true
87+
}},
88+
]),
7689
ruby(),
7790
oneDark,
7891
EditorView.theme({

package-lock.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codemirror-lang-ruby",
3-
"version": "0.1.0",
3+
"version": "0.2.0",
44
"description": "Ruby language support for CodeMirror 6, built on a Lezer grammar",
55
"type": "module",
66
"main": "dist/index.cjs",
@@ -45,8 +45,10 @@
4545
},
4646
"devDependencies": {
4747
"@codemirror/autocomplete": "^6.20.1",
48+
"@codemirror/commands": "^6.10.3",
4849
"@codemirror/language": "^6.0.0",
4950
"@codemirror/theme-one-dark": "^6.1.3",
51+
"@codemirror/view": "^6.40.0",
5052
"@lezer/common": "^1.0.0",
5153
"@lezer/generator": "^1.0.0",
5254
"@lezer/highlight": "^1.0.0",

0 commit comments

Comments
 (0)