rfmt 0.4.1 → 1.0.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: f49bc8ba6c22392ac19d4f52cb9f82ee0476f849326745d821b952a75b13bb70
4
- data.tar.gz: 59f68f137d3b769e17463c8c47b5ee3add7c06234caf50b3a20c03a34bf32cc0
3
+ metadata.gz: 75dece5f16cef2420a328e75f674416858eef7871c99a6aec1136c73f4b3b44c
4
+ data.tar.gz: f514b13a111e7113ed68beb1a1443b7815f8b11d47deee454ef352acf3e5ec7d
5
5
  SHA512:
6
- metadata.gz: 8bd5393bab836f18a889ddac8e00e997bc7e1d45a6e9b2e5f4d232dd9589ce367ed76c60129631ca6a1626ba76031a8517cdd96e05ad41ebe960468f9933fb1e
7
- data.tar.gz: f6692b5a16f284c59e5d8281c5bb11e96ce6f70aee47f6656c8e13986e20b27874f2dbcbee1168a6314a67f234909bb4e94a55903bcecddd072c1b9804475212
6
+ metadata.gz: 3a3575ed8e1e35770b6ac016521ee131fc1cf92b065c16ae91418a1835e7ab6695f30b9f1bb09c2b45c24801ae9427490ae835fffca228137c18ca1299416bf2
7
+ data.tar.gz: a5459089d49f7b905e565a3289dae2076df6a3d472a1332c0cca8f15ebab8d712a571e202b7053fcbbaeb85431d2f14f586e272e70966a0eeec1b72002e3fe4a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.0.0] - 2025-12-11
4
+
5
+ ### Breaking Changes
6
+ - First stable release (v1.0.0)
7
+
8
+ ### Changed
9
+ - Set JSON as default output format
10
+ - Updated Japanese documentation
11
+ - Code formatting improvements
12
+
13
+ ### Fixed
14
+ - TOML configuration parsing fix
15
+ - Logger initialization fix
16
+
17
+ ## [0.5.0] - 2025-12-07
18
+
19
+ ### Changed
20
+ - Synchronized markdown command documentation
21
+ - Added project logo
22
+
23
+ ### Fixed
24
+ - Removed unnecessary exec command
25
+
3
26
  ## [0.4.1] - 2025-11-28
4
27
 
5
28
  ### Fixed
data/Cargo.lock CHANGED
@@ -1219,7 +1219,7 @@ checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
1219
1219
 
1220
1220
  [[package]]
1221
1221
  name = "rfmt"
1222
- version = "0.4.1"
1222
+ version = "1.0.0"
1223
1223
  dependencies = [
1224
1224
  "anyhow",
1225
1225
  "clap",
data/README.md CHANGED
@@ -13,6 +13,8 @@ A Ruby code formatter written in Rust
13
13
  [Documentation](#documentation) •
14
14
  [Contributing](#contributing)
15
15
 
16
+ <a href="https://flatt.tech/oss/gmo/trampoline" target="_blank"><img src="https://flatt.tech/assets/images/badges/gmo-oss.svg" height="24px"/></a>
17
+
16
18
  </div>
17
19
 
18
20
  ---
@@ -154,13 +156,13 @@ rfmt init --force
154
156
  Format a single file:
155
157
 
156
158
  ```bash
157
- rfmt exec lib/user.rb
159
+ rfmt lib/user.rb
158
160
  ```
159
161
 
160
162
  Format multiple files:
161
163
 
162
164
  ```bash
163
- rfmt exec lib/**/*.rb
165
+ rfmt lib/**/*.rb
164
166
  ```
165
167
 
166
168
  Check if files need formatting (CI/CD):
@@ -172,15 +174,15 @@ rfmt check .
172
174
  Show diff without modifying files:
173
175
 
174
176
  ```bash
175
- rfmt exec lib/user.rb --diff
177
+ rfmt lib/user.rb --diff
176
178
  ```
177
179
 
178
180
  Enable verbose output for debugging:
179
181
 
180
182
  ```bash
181
- rfmt exec lib/user.rb --verbose
183
+ rfmt lib/user.rb --verbose
182
184
  # or use environment variable
183
- DEBUG=1 rfmt exec lib/user.rb
185
+ DEBUG=1 rfmt lib/user.rb
184
186
  ```
185
187
 
186
188
  ### Ruby API
data/exe/rfmt CHANGED
@@ -3,8 +3,14 @@
3
3
 
4
4
  require 'rfmt/cli'
5
5
 
6
+ # Known subcommands - if first arg is not one of these, prepend 'format'
7
+ SUBCOMMANDS = %w[format check version config cache init help].freeze
8
+
9
+ args = ARGV.dup
10
+ args.unshift('format') if args.empty? || (!SUBCOMMANDS.include?(args.first) && !args.first.start_with?('-'))
11
+
6
12
  begin
7
- Rfmt::CLI.start(ARGV)
13
+ Rfmt::CLI.start(args)
8
14
  rescue Interrupt
9
15
  puts "\nInterrupted"
10
16
  exit(130)
data/ext/rfmt/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rfmt"
3
- version = "0.4.1"
3
+ version = "1.0.0"
4
4
  edition = "2021"
5
5
  authors = ["fujitani sora <fujitanisora0414@gmail.com>"]
6
6
  license = "MIT"
@@ -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
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
 
data/lib/rfmt/cli.rb CHANGED
@@ -47,7 +47,9 @@ module Rfmt
47
47
  class_option :config, type: :string, desc: 'Path to configuration file'
48
48
  class_option :verbose, type: :boolean, aliases: '-v', desc: 'Verbose output'
49
49
 
50
- desc 'exec [FILES]', 'Format Ruby files'
50
+ default_command :format
51
+
52
+ desc 'format [FILES]', 'Format Ruby files (default command)'
51
53
  option :write, type: :boolean, default: true, desc: 'Write formatted output'
52
54
  option :check, type: :boolean, desc: "Check if files are formatted (don't write)"
53
55
  option :diff, type: :boolean, desc: 'Show diff of changes'
@@ -56,7 +58,7 @@ module Rfmt
56
58
  option :jobs, type: :numeric, desc: 'Number of parallel jobs (default: CPU count)'
57
59
  option :cache, type: :boolean, default: true, desc: 'Use cache to skip unchanged files'
58
60
  option :cache_dir, type: :string, desc: 'Cache directory (default: ~/.cache/rfmt)'
59
- def exec(*files)
61
+ def format(*files)
60
62
  config = load_config
61
63
  files = files.empty? ? config.files_to_format : files.flatten
62
64
 
@@ -101,7 +103,7 @@ module Rfmt
101
103
 
102
104
  desc 'check [FILES]', 'Check if files need formatting'
103
105
  def check(*files)
104
- invoke :exec, files, check: true, write: false
106
+ invoke :format, files, check: true, write: false
105
107
  end
106
108
 
107
109
  desc 'version', 'Show version'
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.4.1'
4
+ VERSION = '1.0.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.4.1
4
+ version: 1.0.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-11-27 00:00:00.000000000 Z
11
+ date: 2025-12-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rb_sys
@@ -62,6 +62,8 @@ files:
62
62
  - lib/rfmt/prism_node_extractor.rb
63
63
  - lib/rfmt/rfmt.so
64
64
  - lib/rfmt/version.rb
65
+ - lib/ruby_lsp/rfmt/addon.rb
66
+ - lib/ruby_lsp/rfmt/formatter_runner.rb
65
67
  homepage: https://github.com/fs0414/rfmt
66
68
  licenses:
67
69
  - MIT
@@ -70,6 +72,7 @@ metadata:
70
72
  homepage_uri: https://github.com/fs0414/rfmt
71
73
  source_code_uri: https://github.com/fs0414/rfmt
72
74
  changelog_uri: https://github.com/fs0414/rfmt/releases
75
+ ruby_lsp_addon: 'true'
73
76
  post_install_message:
74
77
  rdoc_options: []
75
78
  require_paths: