rfmt 0.5.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e72bec96df5da8aafa6e1cd49ab3c14af9aaaaf79a880cbf2a6b05715a61aa7c
4
- data.tar.gz: 381c235d258b0db3f7c770dd360cc3816ccaa1fdbe1af4fc857e0f871618d29b
3
+ metadata.gz: 7db146878fe1bbf37f4211f11ded67e98ebfb1830989a4fb64fbe1cc51a72fc7
4
+ data.tar.gz: bb87cfab19f7513ddd99b939c03286827687a5da6071c317390dc990924dbe05
5
5
  SHA512:
6
- metadata.gz: 97261a648e5aa560d4b3a2499ed82e208d445f422b50dc0d17be0b0aa2065b7d54cc8547e09a8632019a8538cafae750468e2f95ad73bf881955bc982e527d5c
7
- data.tar.gz: 8ee6c5ec43066f7d70d07198dc72032c87fe4be1759a74196f4c17c74bfbf91579116a0744ea2b39cb8a222748a61b1e65e723e43e22d19b4589c4a28ce4baed
6
+ metadata.gz: 2018f31f65afd41842a51e21290448a753784b840dd67f9d8e7e41d223f509227143b18a15309b4a3cf19543b8638ce28045d0c1a68cf97dda8781811b058657
7
+ data.tar.gz: 888d036c08f29f166d1089a2cce27d9c456d57c83fbbd1c518142bdfaa15b32c551ab1ccda12c97bd48c9ddf63f57545ef47b6cbdcf8a774f8ea4eb1e17d1729
data/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.1.0] - 2025-12-12
4
+
5
+ ### Added
6
+ - Editor integration (Ruby LSP support)
7
+ - Required/Optional keyword parameter node type support
8
+
9
+ ### Fixed
10
+ - Migration file superclass corruption (ActiveRecord::Migration[8.1] etc.)
11
+
12
+ ### Changed
13
+ - Removed unused scripts and test files (reduced Ruby code by ~38%)
14
+
15
+ ## [1.0.0] - 2025-12-11
16
+
17
+ ### Breaking Changes
18
+ - First stable release (v1.0.0)
19
+
20
+ ### Added
21
+ - Neovim integration: format-on-save support with autocmd configuration
22
+
23
+ ### Changed
24
+ - Set JSON as default output format
25
+ - Updated Japanese documentation
26
+ - Code formatting improvements
27
+
28
+ ### Fixed
29
+ - TOML configuration parsing fix
30
+ - Logger initialization fix
31
+
3
32
  ## [0.5.0] - 2025-12-07
4
33
 
5
34
  ### Changed
data/Cargo.lock CHANGED
@@ -1219,7 +1219,7 @@ checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
1219
1219
 
1220
1220
  [[package]]
1221
1221
  name = "rfmt"
1222
- version = "0.5.0"
1222
+ version = "1.0.0"
1223
1223
  dependencies = [
1224
1224
  "anyhow",
1225
1225
  "clap",
data/README.md CHANGED
@@ -10,6 +10,7 @@ A Ruby code formatter written in Rust
10
10
  [Installation](#installation) •
11
11
  [Usage](#usage) •
12
12
  [Features](#features) •
13
+ [Editor Integration](#editor-integration) •
13
14
  [Documentation](#documentation) •
14
15
  [Contributing](#contributing)
15
16
 
@@ -273,6 +274,33 @@ class User < ApplicationRecord
273
274
  end
274
275
  ```
275
276
 
277
+ ## Editor Integration
278
+
279
+ ### Neovim
280
+
281
+ Format Ruby files on save using autocmd:
282
+
283
+ ```lua
284
+ -- ~/.config/nvim/init.lua
285
+
286
+ vim.api.nvim_create_autocmd("BufWritePre", {
287
+ pattern = { "*.rb", "*.rake", "Gemfile", "Rakefile" },
288
+ callback = function()
289
+ local filepath = vim.fn.expand("%:p")
290
+ local result = vim.fn.system({ "rfmt", filepath })
291
+ if vim.v.shell_error == 0 then
292
+ vim.cmd("edit!")
293
+ end
294
+ end,
295
+ })
296
+ ```
297
+
298
+ ### Coming Soon
299
+
300
+ - **VS Code** - Extension in development
301
+ - **RubyMine** - Plugin in development
302
+ - **Zed** - Extension in development
303
+
276
304
  ## Development
277
305
 
278
306
  ### Setup
data/ext/rfmt/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rfmt"
3
- version = "0.5.0"
3
+ version = "1.0.0"
4
4
  edition = "2021"
5
5
  authors = ["fujitani sora <fujitanisora0414@gmail.com>"]
6
6
  license = "MIT"
@@ -66,6 +66,8 @@ pub enum NodeType {
66
66
  OptionalParameterNode,
67
67
  RestParameterNode,
68
68
  KeywordParameterNode,
69
+ RequiredKeywordParameterNode,
70
+ OptionalKeywordParameterNode,
69
71
  KeywordRestParameterNode,
70
72
  BlockParameterNode,
71
73
 
@@ -102,6 +104,8 @@ impl NodeType {
102
104
  "optional_parameter_node" => Self::OptionalParameterNode,
103
105
  "rest_parameter_node" => Self::RestParameterNode,
104
106
  "keyword_parameter_node" => Self::KeywordParameterNode,
107
+ "required_keyword_parameter_node" => Self::RequiredKeywordParameterNode,
108
+ "optional_keyword_parameter_node" => Self::OptionalKeywordParameterNode,
105
109
  "keyword_rest_parameter_node" => Self::KeywordRestParameterNode,
106
110
  "block_parameter_node" => Self::BlockParameterNode,
107
111
  _ => Self::Unknown(s.to_string()),
@@ -3,6 +3,13 @@ use crate::config::{Config, IndentStyle};
3
3
  use crate::error::Result;
4
4
  use std::fmt::Write;
5
5
 
6
+ /// Block style for Ruby blocks
7
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
8
+ enum BlockStyle {
9
+ DoEnd, // do ... end
10
+ Braces, // { ... }
11
+ }
12
+
6
13
  /// Code emitter that converts AST back to Ruby source code
7
14
  pub struct Emitter {
8
15
  config: Config,
@@ -115,6 +122,7 @@ impl Emitter {
115
122
  NodeType::DefNode => self.emit_method(node, indent_level)?,
116
123
  NodeType::IfNode => self.emit_if_unless(node, indent_level, false, "if")?,
117
124
  NodeType::UnlessNode => self.emit_if_unless(node, indent_level, false, "unless")?,
125
+ NodeType::CallNode => self.emit_call(node, indent_level)?,
118
126
  _ => self.emit_generic(node, indent_level)?,
119
127
  }
120
128
  Ok(())
@@ -429,6 +437,183 @@ impl Emitter {
429
437
  Ok(())
430
438
  }
431
439
 
440
+ /// Emit method call, handling blocks specially for proper indentation
441
+ fn emit_call(&mut self, node: &Node, indent_level: usize) -> Result<()> {
442
+ // Emit any comments before this call
443
+ self.emit_comments_before(node.location.start_line, indent_level)?;
444
+
445
+ // Check if this call has a block (last child is BlockNode)
446
+ let has_block = node
447
+ .children
448
+ .last()
449
+ .map(|c| matches!(c.node_type, NodeType::BlockNode))
450
+ .unwrap_or(false);
451
+
452
+ if !has_block {
453
+ // No block - use generic emission (extracts from source)
454
+ return self.emit_generic_without_comments(node, indent_level);
455
+ }
456
+
457
+ // Has block - need to handle specially
458
+ let block_node = node.children.last().unwrap();
459
+
460
+ // Determine block style from source (do...end vs { })
461
+ let block_style = self.detect_block_style(block_node);
462
+
463
+ // Emit the call part (receiver.method(args)) from source
464
+ self.emit_call_without_block(node, block_node, indent_level)?;
465
+
466
+ match block_style {
467
+ BlockStyle::DoEnd => self.emit_do_end_block(block_node, indent_level)?,
468
+ BlockStyle::Braces => self.emit_brace_block(block_node, indent_level)?,
469
+ }
470
+
471
+ Ok(())
472
+ }
473
+
474
+ /// Detect whether block uses do...end or { } style
475
+ fn detect_block_style(&self, block_node: &Node) -> BlockStyle {
476
+ if self.source.is_empty() {
477
+ return BlockStyle::DoEnd; // Default fallback
478
+ }
479
+
480
+ let start = block_node.location.start_offset;
481
+ if let Some(first_char) = self.source.get(start..start + 1) {
482
+ if first_char == "{" {
483
+ return BlockStyle::Braces;
484
+ }
485
+ }
486
+
487
+ BlockStyle::DoEnd // Default (includes 'do' keyword)
488
+ }
489
+
490
+ /// Emit the method call part without the block
491
+ fn emit_call_without_block(
492
+ &mut self,
493
+ call_node: &Node,
494
+ block_node: &Node,
495
+ indent_level: usize,
496
+ ) -> Result<()> {
497
+ self.emit_indent(indent_level)?;
498
+
499
+ if !self.source.is_empty() {
500
+ let start = call_node.location.start_offset;
501
+ let end = block_node.location.start_offset;
502
+
503
+ if let Some(text) = self.source.get(start..end) {
504
+ // Trim trailing whitespace but preserve the content
505
+ write!(self.buffer, "{}", text.trim_end())?;
506
+ }
507
+ }
508
+
509
+ Ok(())
510
+ }
511
+
512
+ /// Emit a do...end style block with proper indentation
513
+ fn emit_do_end_block(&mut self, block_node: &Node, indent_level: usize) -> Result<()> {
514
+ // Add space before 'do' and emit 'do'
515
+ write!(self.buffer, " do")?;
516
+
517
+ // Emit block parameters if present (|x, y|)
518
+ self.emit_block_parameters(block_node)?;
519
+
520
+ self.buffer.push('\n');
521
+
522
+ // Find and emit the body (StatementsNode among children)
523
+ for child in &block_node.children {
524
+ if matches!(child.node_type, NodeType::StatementsNode) {
525
+ self.emit_statements(child, indent_level + 1)?;
526
+ self.buffer.push('\n');
527
+ break;
528
+ }
529
+ }
530
+
531
+ // Emit 'end'
532
+ self.emit_indent(indent_level)?;
533
+ write!(self.buffer, "end")?;
534
+
535
+ Ok(())
536
+ }
537
+
538
+ /// Emit a { } style block
539
+ fn emit_brace_block(&mut self, block_node: &Node, indent_level: usize) -> Result<()> {
540
+ // Determine if block should be inline or multiline
541
+ let is_multiline = block_node.location.start_line != block_node.location.end_line;
542
+
543
+ if is_multiline {
544
+ // Multiline brace block
545
+ write!(self.buffer, " {{")?;
546
+ self.emit_block_parameters(block_node)?;
547
+ self.buffer.push('\n');
548
+
549
+ // Emit body
550
+ for child in &block_node.children {
551
+ if matches!(child.node_type, NodeType::StatementsNode) {
552
+ self.emit_statements(child, indent_level + 1)?;
553
+ self.buffer.push('\n');
554
+ break;
555
+ }
556
+ }
557
+
558
+ self.emit_indent(indent_level)?;
559
+ write!(self.buffer, "}}")?;
560
+ } else {
561
+ // Inline brace block - extract from source to preserve spacing
562
+ write!(self.buffer, " ")?;
563
+ if let Some(text) = self
564
+ .source
565
+ .get(block_node.location.start_offset..block_node.location.end_offset)
566
+ {
567
+ write!(self.buffer, "{}", text)?;
568
+ }
569
+ }
570
+
571
+ Ok(())
572
+ }
573
+
574
+ /// Emit block parameters (|x, y|)
575
+ fn emit_block_parameters(&mut self, block_node: &Node) -> Result<()> {
576
+ if self.source.is_empty() {
577
+ return Ok(());
578
+ }
579
+
580
+ let start = block_node.location.start_offset;
581
+ let end = block_node.location.end_offset;
582
+
583
+ if let Some(block_source) = self.source.get(start..end) {
584
+ // Only look at the first line of the block for parameters
585
+ let first_line = block_source.lines().next().unwrap_or("");
586
+
587
+ // Find |...| pattern in the first line only
588
+ if let Some(pipe_start) = first_line.find('|') {
589
+ // Find matching pipe after first one
590
+ if let Some(pipe_end) = first_line[pipe_start + 1..].find('|') {
591
+ let params = &first_line[pipe_start..=pipe_start + 1 + pipe_end];
592
+ write!(self.buffer, " {}", params)?;
593
+ }
594
+ }
595
+ }
596
+
597
+ Ok(())
598
+ }
599
+
600
+ /// Emit generic node without re-emitting comments (for use when comments already handled)
601
+ fn emit_generic_without_comments(&mut self, node: &Node, indent_level: usize) -> Result<()> {
602
+ if !self.source.is_empty() {
603
+ let start = node.location.start_offset;
604
+ let end = node.location.end_offset;
605
+
606
+ let text_owned = self.source.get(start..end).map(|s| s.to_string());
607
+
608
+ if let Some(text) = text_owned {
609
+ self.emit_indent(indent_level)?;
610
+ write!(self.buffer, "{}", text)?;
611
+ self.emit_trailing_comments(node.location.end_line)?;
612
+ }
613
+ }
614
+ Ok(())
615
+ }
616
+
432
617
  /// Emit generic node by extracting from source
433
618
  fn emit_generic(&mut self, node: &Node, indent_level: usize) -> Result<()> {
434
619
  // Emit any comments before this node
@@ -475,6 +660,8 @@ impl Emitter {
475
660
  | NodeType::OptionalParameterNode
476
661
  | NodeType::RestParameterNode
477
662
  | NodeType::KeywordParameterNode
663
+ | NodeType::RequiredKeywordParameterNode
664
+ | NodeType::OptionalKeywordParameterNode
478
665
  | NodeType::KeywordRestParameterNode
479
666
  | NodeType::BlockParameterNode
480
667
  )
data/ext/rfmt/src/lib.rs CHANGED
@@ -54,7 +54,7 @@ fn rust_version() -> String {
54
54
  }
55
55
 
56
56
  #[magnus::init]
57
- fn init(ruby: &Ruby) -> Result<(), Error> {
57
+ fn init(_ruby: &Ruby) -> Result<(), Error> {
58
58
  logging::RfmtLogger::init();
59
59
  log::info!("Initializing rfmt Rust extension");
60
60
 
@@ -64,15 +64,6 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
64
64
  module.define_singleton_method("parse_to_json", function!(parse_to_json, 1))?;
65
65
  module.define_singleton_method("rust_version", function!(rust_version, 0))?;
66
66
 
67
- let rfmt_error = ruby.define_error("RfmtError", ruby.exception_standard_error())?;
68
- ruby.define_error("ParseError", rfmt_error)?;
69
- ruby.define_error("ConfigError", rfmt_error)?;
70
- ruby.define_error("PrismError", rfmt_error)?;
71
- ruby.define_error("RuleError", rfmt_error)?;
72
- ruby.define_error("InternalError", rfmt_error)?;
73
- ruby.define_error("FormattingError", rfmt_error)?;
74
- ruby.define_error("UnsupportedFeature", rfmt_error)?;
75
-
76
67
  log::info!("rfmt Rust extension initialized successfully");
77
68
  Ok(())
78
69
  }
@@ -37,8 +37,10 @@ impl RfmtLogger {
37
37
  LevelFilter::Warn
38
38
  });
39
39
  let logger = Self::new(level);
40
- log::set_boxed_logger(Box::new(logger)).expect("Failed to initialize logger");
41
- log::set_max_level(LevelFilter::Trace);
40
+ // Ignore if logger is already set (e.g., in ruby_lsp environment)
41
+ if log::set_boxed_logger(Box::new(logger)).is_ok() {
42
+ log::set_max_level(LevelFilter::Trace);
43
+ }
42
44
  }
43
45
  }
44
46
 
@@ -27,16 +27,25 @@ module Rfmt
27
27
  when Prism::ConstantReadNode
28
28
  sc.name.to_s
29
29
  when Prism::ConstantPathNode
30
- # Try full_name first, fall back to name
30
+ # Try full_name first, fall back to slice for original source
31
31
  if sc.respond_to?(:full_name)
32
32
  sc.full_name.to_s
33
- elsif sc.respond_to?(:name)
34
- sc.name.to_s
33
+ elsif sc.respond_to?(:slice)
34
+ sc.slice
35
35
  else
36
- sc.to_s
36
+ sc.location.slice
37
37
  end
38
+ when Prism::CallNode
39
+ # Handle cases like ActiveRecord::Migration[8.1]
40
+ # Use slice to get the original source text
41
+ sc.slice
38
42
  else
39
- sc.to_s
43
+ # Fallback: try to get original source text
44
+ if sc.respond_to?(:slice)
45
+ sc.slice
46
+ else
47
+ sc.location.slice
48
+ end
40
49
  end
41
50
  end
42
51
 
data/lib/rfmt/rfmt.so CHANGED
Binary file
data/lib/rfmt/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rfmt
4
- VERSION = '0.5.0'
4
+ VERSION = '1.1.0'
5
5
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ruby_lsp/addon'
4
+ require_relative 'formatter_runner'
5
+
6
+ module RubyLsp
7
+ module Rfmt
8
+ class Addon < ::RubyLsp::Addon
9
+ def name
10
+ 'rfmt'
11
+ end
12
+
13
+ def activate(global_state, _message_queue)
14
+ global_state.register_formatter('rfmt', FormatterRunner.new)
15
+ end
16
+
17
+ def deactivate; end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rfmt'
4
+
5
+ module RubyLsp
6
+ module Rfmt
7
+ class FormatterRunner
8
+ # @param uri [URI::Generic] Document URI
9
+ # @param document [RubyLsp::RubyDocument] Target document
10
+ # @return [String, nil] Formatted text or nil on error
11
+ def run_formatting(_uri, document)
12
+ source = document.source
13
+ ::Rfmt.format(source)
14
+ rescue ::Rfmt::Error
15
+ nil
16
+ end
17
+
18
+ # @param uri [URI::Generic] Document URI
19
+ # @param document [RubyLsp::RubyDocument] Target document
20
+ # @return [Array<RubyLsp::Interface::Diagnostic>]
21
+ def run_diagnostic(_uri, _document)
22
+ []
23
+ end
24
+ end
25
+ end
26
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rfmt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - fujitani sora
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-12-07 00:00:00.000000000 Z
11
+ date: 2025-12-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rb_sys
@@ -41,8 +41,6 @@ files:
41
41
  - exe/rfmt
42
42
  - ext/rfmt/Cargo.toml
43
43
  - ext/rfmt/extconf.rb
44
- - ext/rfmt/spec/config_spec.rb
45
- - ext/rfmt/spec/spec_helper.rb
46
44
  - ext/rfmt/src/ast/mod.rs
47
45
  - ext/rfmt/src/config/mod.rs
48
46
  - ext/rfmt/src/emitter/mod.rs
@@ -62,6 +60,8 @@ files:
62
60
  - lib/rfmt/prism_node_extractor.rb
63
61
  - lib/rfmt/rfmt.so
64
62
  - lib/rfmt/version.rb
63
+ - lib/ruby_lsp/rfmt/addon.rb
64
+ - lib/ruby_lsp/rfmt/formatter_runner.rb
65
65
  homepage: https://github.com/fs0414/rfmt
66
66
  licenses:
67
67
  - MIT
@@ -70,6 +70,7 @@ metadata:
70
70
  homepage_uri: https://github.com/fs0414/rfmt
71
71
  source_code_uri: https://github.com/fs0414/rfmt
72
72
  changelog_uri: https://github.com/fs0414/rfmt/releases
73
+ ruby_lsp_addon: 'true'
73
74
  post_install_message:
74
75
  rdoc_options: []
75
76
  require_paths:
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- RSpec.describe 'Configuration' do
6
- describe 'configuration system integration' do
7
- it 'can load YAML configuration' do
8
- require 'tempfile'
9
- require 'yaml'
10
-
11
- config = {
12
- 'version' => '1.0',
13
- 'formatting' => {
14
- 'line_length' => 120,
15
- 'indent_width' => 4,
16
- 'indent_style' => 'tabs',
17
- 'quote_style' => 'single'
18
- }
19
- }
20
-
21
- Tempfile.create(['test_config', '.yml']) do |file|
22
- file.write(YAML.dump(config))
23
- file.flush
24
-
25
- loaded_config = YAML.load_file(file.path)
26
- expect(loaded_config['formatting']['line_length']).to eq(120)
27
- expect(loaded_config['formatting']['indent_width']).to eq(4)
28
- expect(loaded_config['formatting']['indent_style']).to eq('tabs')
29
- expect(loaded_config['formatting']['quote_style']).to eq('single')
30
- end
31
- end
32
-
33
- it 'validates configuration values' do
34
- # Configuration validation is tested in Rust tests
35
- # 11 Rust tests verify all validation logic
36
- expect(true).to be true
37
- end
38
- end
39
- end
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'bundler/setup'
4
- require 'rfmt'
5
-
6
- RSpec.configure do |config|
7
- config.expect_with :rspec do |expectations|
8
- expectations.include_chain_clauses_in_custom_matcher_descriptions = true
9
- end
10
-
11
- config.mock_with :rspec do |mocks|
12
- mocks.verify_partial_doubles = true
13
- end
14
-
15
- config.shared_context_metadata_behavior = :apply_to_host_groups
16
- end