rfmt 1.6.3-aarch64-linux → 2.0.0.beta1-aarch64-linux

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.
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Rfmt
6
+ module LSP
7
+ # Reads and writes LSP JSON-RPC messages over an IO pair.
8
+ class MessageIO
9
+ HEADER_SEPARATOR = "\r\n\r\n"
10
+ CONTENT_LENGTH = /\AContent-Length:\s*(\d+)\z/i
11
+
12
+ def initialize(input: $stdin, output: $stdout)
13
+ @input = input
14
+ @output = output
15
+ end
16
+
17
+ def read_message
18
+ headers = read_headers
19
+ return nil if headers.nil?
20
+
21
+ content_length = headers.fetch('content-length').to_i
22
+ JSON.parse(@input.read(content_length))
23
+ end
24
+
25
+ def write_message(payload)
26
+ body = JSON.generate(payload)
27
+ @output.write("Content-Length: #{body.bytesize}#{HEADER_SEPARATOR}#{body}")
28
+ @output.flush if @output.respond_to?(:flush)
29
+ end
30
+
31
+ private
32
+
33
+ def read_headers
34
+ headers = {}
35
+
36
+ loop do
37
+ line = @input.gets
38
+ return nil if line.nil?
39
+
40
+ line = line.chomp
41
+ line = line.delete_suffix("\r")
42
+ break if line.empty?
43
+
44
+ match = CONTENT_LENGTH.match(line)
45
+ headers['content-length'] = match[1] if match
46
+ end
47
+
48
+ headers.fetch('content-length')
49
+ headers
50
+ rescue KeyError
51
+ nil
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'language_server/protocol'
4
+ require 'rfmt/version'
5
+
6
+ require_relative 'document_store'
7
+ require_relative 'formatter'
8
+ require_relative 'message_io'
9
+ require_relative 'uri'
10
+ require_relative 'workspace'
11
+
12
+ module Rfmt
13
+ module LSP
14
+ class Server
15
+ TEXT_DOCUMENT_SYNC_FULL = 1
16
+ METHOD_NOT_FOUND = -32_601
17
+ INTERNAL_ERROR = -32_603
18
+
19
+ def initialize(input: $stdin, output: $stdout)
20
+ @io = MessageIO.new(input: input, output: output)
21
+ @documents = DocumentStore.new
22
+ @workspace = Workspace.new
23
+ @shutdown_requested = false
24
+ end
25
+
26
+ def run
27
+ while (message = @io.read_message)
28
+ return @shutdown_requested ? 0 : 1 if handle_message(message) == :exit
29
+ end
30
+
31
+ 0
32
+ end
33
+
34
+ def handle_message(message)
35
+ method = message['method']
36
+ id = message['id']
37
+ params = message['params'] || {}
38
+
39
+ dispatch_message(method, id, params)
40
+ rescue StandardError => e
41
+ respond_error(id, INTERNAL_ERROR, e.message) if id
42
+ end
43
+
44
+ private
45
+
46
+ def dispatch_message(method, id, params)
47
+ if request_handler?(method)
48
+ dispatch_request(method, id, params)
49
+ else
50
+ dispatch_notification(method, params) || respond_method_not_found(id, method)
51
+ end
52
+ end
53
+
54
+ def request_handler?(method)
55
+ %w[initialize textDocument/formatting shutdown].include?(method)
56
+ end
57
+
58
+ def dispatch_request(method, id, params)
59
+ case method
60
+ when 'initialize'
61
+ handle_initialize(id, params)
62
+ when 'textDocument/formatting'
63
+ handle_formatting(id, params)
64
+ when 'shutdown'
65
+ handle_shutdown(id)
66
+ end
67
+ end
68
+
69
+ def dispatch_notification(method, params)
70
+ case method
71
+ when 'initialized'
72
+ true
73
+ when 'textDocument/didOpen'
74
+ handle_did_open(params)
75
+ true
76
+ when 'textDocument/didChange'
77
+ handle_did_change(params)
78
+ true
79
+ when 'textDocument/didClose'
80
+ handle_did_close(params)
81
+ true
82
+ when 'exit'
83
+ :exit
84
+ end
85
+ end
86
+
87
+ def respond_method_not_found(id, method)
88
+ respond_error(id, METHOD_NOT_FOUND, "Method not found: #{method}") if id
89
+ end
90
+
91
+ def handle_initialize(id, params)
92
+ @workspace.configure(params)
93
+
94
+ respond(id, {
95
+ capabilities: {
96
+ documentFormattingProvider: true,
97
+ textDocumentSync: TEXT_DOCUMENT_SYNC_FULL
98
+ },
99
+ serverInfo: {
100
+ name: 'rfmt',
101
+ version: Rfmt::VERSION
102
+ }
103
+ })
104
+ end
105
+
106
+ def handle_did_open(params)
107
+ text_document = params.fetch('textDocument')
108
+ @documents.open(text_document.fetch('uri'), text_document.fetch('text'))
109
+ end
110
+
111
+ def handle_did_change(params)
112
+ uri = params.fetch('textDocument').fetch('uri')
113
+ change = Array(params['contentChanges']).last
114
+ return unless change&.key?('text')
115
+
116
+ @documents.change(uri, change.fetch('text'))
117
+ end
118
+
119
+ def handle_did_close(params)
120
+ uri = params.fetch('textDocument').fetch('uri')
121
+ @documents.close(uri)
122
+ end
123
+
124
+ def handle_formatting(id, params)
125
+ uri = params.fetch('textDocument').fetch('uri')
126
+ source = @documents.source_for(uri) || read_file_source(uri)
127
+ edits = if source
128
+ @workspace.with_root_for(uri) { Formatter.format_edits(source) }
129
+ else
130
+ []
131
+ end
132
+
133
+ respond(id, edits)
134
+ end
135
+
136
+ def handle_shutdown(id)
137
+ @shutdown_requested = true
138
+ respond(id, nil)
139
+ end
140
+
141
+ def read_file_source(uri)
142
+ path = URI.file_uri_to_path(uri)
143
+ return nil unless path && File.file?(path)
144
+
145
+ File.read(path)
146
+ end
147
+
148
+ def respond(id, result)
149
+ @io.write_message({
150
+ jsonrpc: '2.0',
151
+ id: id,
152
+ result: result
153
+ })
154
+ end
155
+
156
+ def respond_error(id, code, message)
157
+ @io.write_message({
158
+ jsonrpc: '2.0',
159
+ id: id,
160
+ error: {
161
+ code: code,
162
+ message: message
163
+ }
164
+ })
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module Rfmt
6
+ module LSP
7
+ module URI
8
+ module_function
9
+
10
+ def file_uri_to_path(uri)
11
+ parsed = ::URI.parse(uri)
12
+ return nil unless parsed.scheme == 'file'
13
+
14
+ percent_decode(parsed.path)
15
+ rescue ::URI::InvalidURIError
16
+ nil
17
+ end
18
+
19
+ def path_to_file_uri(path)
20
+ "file://#{percent_encode(File.expand_path(path))}"
21
+ end
22
+
23
+ def percent_decode(value)
24
+ value.gsub(/%[0-9A-Fa-f]{2}/) { |match| [match[1..].to_i(16)].pack('C') }
25
+ end
26
+
27
+ def percent_encode(value)
28
+ value.bytes.map do |byte|
29
+ char = byte.chr
30
+ char.match?(%r{[A-Za-z0-9._~/-]}) ? char : format('%%%02X', byte)
31
+ end.join
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ require_relative 'uri'
6
+
7
+ module Rfmt
8
+ module LSP
9
+ class Workspace
10
+ def initialize
11
+ @roots = []
12
+ end
13
+
14
+ def configure(params)
15
+ @roots = workspace_folder_roots(params)
16
+ root_uri = params['rootUri']
17
+ @roots << URI.file_uri_to_path(root_uri) if root_uri
18
+ @roots = @roots.compact.map { |root| File.expand_path(root) }.uniq
19
+ end
20
+
21
+ def root_for(uri)
22
+ path = URI.file_uri_to_path(uri)
23
+ return existing_root(@roots.first) unless path
24
+
25
+ matching_root = @roots
26
+ .select { |root| path_inside?(path, root) }
27
+ .max_by(&:length)
28
+ return existing_root(matching_root) if matching_root
29
+
30
+ existing_root(File.dirname(path))
31
+ end
32
+
33
+ def with_root_for(uri, &block)
34
+ root = root_for(uri)
35
+ return block.call unless root
36
+
37
+ Dir.chdir(root, &block)
38
+ end
39
+
40
+ private
41
+
42
+ def workspace_folder_roots(params)
43
+ Array(params['workspaceFolders']).filter_map do |folder|
44
+ URI.file_uri_to_path(folder['uri'])
45
+ end
46
+ end
47
+
48
+ def path_inside?(path, root)
49
+ relative = Pathname.new(path).relative_path_from(Pathname.new(root)).to_s
50
+ relative == '.' || !relative.start_with?('..')
51
+ rescue ArgumentError
52
+ false
53
+ end
54
+
55
+ def existing_root(root)
56
+ return nil unless root && File.directory?(root)
57
+
58
+ root
59
+ end
60
+ end
61
+ end
62
+ end
data/lib/rfmt/lsp.rb ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lsp/document_store'
4
+ require_relative 'lsp/formatter'
5
+ require_relative 'lsp/message_io'
6
+ require_relative 'lsp/server'
7
+ require_relative 'lsp/uri'
8
+ require_relative 'lsp/workspace'
data/lib/rfmt/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rfmt
4
- VERSION = '1.6.3'
4
+ VERSION = '2.0.0.beta1'
5
5
  end
data/lib/rfmt.rb CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  require_relative 'rfmt/version'
4
4
  require_relative 'rfmt/native_extension_loader'
5
- require_relative 'rfmt/prism_bridge'
6
5
 
7
6
  # Load native extension with version-aware loader
8
7
  Rfmt::NativeExtensionLoader.load_extension
@@ -14,25 +13,43 @@ module Rfmt
14
13
  # AST validation errors
15
14
  class ValidationError < RfmtError; end
16
15
 
16
+ # Rust reports errors as plain StandardError with a [Rfmt::<kind>] prefix;
17
+ # these two kinds map onto the public exception classes.
18
+ NATIVE_PARSE_ERROR_PREFIX = '[Rfmt::ParseError] '
19
+ NATIVE_VALIDATION_ERROR_PREFIX = '[Rfmt::ValidationError] '
20
+ NATIVE_CONFIG_ERROR_PREFIX = '[Rfmt::ConfigError] '
21
+ private_constant :NATIVE_PARSE_ERROR_PREFIX, :NATIVE_VALIDATION_ERROR_PREFIX,
22
+ :NATIVE_CONFIG_ERROR_PREFIX
23
+
17
24
  # Format Ruby source code
25
+ # Parsing, config resolution, and output validation all happen natively in Rust
18
26
  # @param source [String] Ruby source code to format
27
+ # @param config_path [String, nil] Explicit config file path; nil discovers
28
+ # rfmt.yml/.rfmt.yml from the current directory upward (cached per process)
19
29
  # @return [String] Formatted Ruby code
20
- def self.format(source)
21
- # Step 1: Parse with Prism (Ruby side)
22
- prism_json = PrismBridge.parse(source)
23
-
24
- # Step 2: Format in Rust
25
- # Pass both source and AST to enable source extraction fallback
26
- format_code(source, prism_json)
27
- rescue PrismBridge::ParseError => e
28
- # Re-raise with more context
29
- raise Error, "Failed to parse Ruby code: #{e.message}"
30
- rescue RfmtError
31
- # Rust side errors are re-raised as-is to preserve error details
32
- raise
30
+ def self.format(source, config_path: nil)
31
+ if config_path
32
+ format_code_with_config(source, config_path.to_s)
33
+ else
34
+ format_code(source)
35
+ end
33
36
  rescue StandardError => e
34
- raise Error, "Unexpected error during formatting: #{e.class}: #{e.message}"
37
+ raise wrap_native_error(e)
38
+ end
39
+
40
+ def self.wrap_native_error(error)
41
+ message = error.message
42
+ if message.start_with?(NATIVE_PARSE_ERROR_PREFIX)
43
+ Error.new("Failed to parse Ruby code: #{message.delete_prefix(NATIVE_PARSE_ERROR_PREFIX)}")
44
+ elsif message.start_with?(NATIVE_VALIDATION_ERROR_PREFIX)
45
+ ValidationError.new(message.delete_prefix(NATIVE_VALIDATION_ERROR_PREFIX))
46
+ elsif message.start_with?(NATIVE_CONFIG_ERROR_PREFIX)
47
+ Error.new("Configuration error: #{message.delete_prefix(NATIVE_CONFIG_ERROR_PREFIX)}")
48
+ else
49
+ Error.new("Unexpected error during formatting: #{error.class}: #{message}")
50
+ end
35
51
  end
52
+ private_class_method :wrap_native_error
36
53
 
37
54
  # Format a Ruby file
38
55
  # @param path [String] Path to Ruby file
@@ -44,6 +61,15 @@ module Rfmt
44
61
  raise Error, "File not found: #{path}"
45
62
  end
46
63
 
64
+ # Effective configuration as the Rust formatter resolves it
65
+ # @param config_path [String, nil] Explicit config file path; nil discovers
66
+ # @return [String] YAML dump of the resolved configuration
67
+ def self.resolved_config(config_path: nil)
68
+ resolved_config_yaml(config_path&.to_s)
69
+ rescue StandardError => e
70
+ raise wrap_native_error(e)
71
+ end
72
+
47
73
  # Get version information
48
74
  # @return [String] Version string including Ruby and Rust versions
49
75
  def self.version_info
@@ -54,8 +80,9 @@ module Rfmt
54
80
  # @param source [String] Ruby source code
55
81
  # @return [String] AST representation
56
82
  def self.parse(source)
57
- prism_json = PrismBridge.parse(source)
58
- parse_to_json(prism_json)
83
+ parse_to_json(source)
84
+ rescue StandardError => e
85
+ raise wrap_native_error(e)
59
86
  end
60
87
 
61
88
  # Configuration management
@@ -114,7 +141,8 @@ module Rfmt
114
141
  current_dir = Dir.pwd
115
142
 
116
143
  loop do
117
- ['.rfmt.yml', '.rfmt.yaml', 'rfmt.yml', 'rfmt.yaml'].each do |filename|
144
+ # Same search order as the Rust side (config/mod.rs CONFIG_FILE_NAMES)
145
+ ['rfmt.yml', 'rfmt.yaml', '.rfmt.yml', '.rfmt.yaml'].each do |filename|
118
146
  config_path = File.join(current_dir, filename)
119
147
  return config_path if File.exist?(config_path)
120
148
  end
@@ -132,7 +160,7 @@ module Rfmt
132
160
  nil
133
161
  end
134
162
  if home_dir
135
- ['.rfmt.yml', '.rfmt.yaml', 'rfmt.yml', 'rfmt.yaml'].each do |filename|
163
+ ['rfmt.yml', 'rfmt.yaml', '.rfmt.yml', '.rfmt.yaml'].each do |filename|
136
164
  config_path = File.join(home_dir, filename)
137
165
  return config_path if File.exist?(config_path)
138
166
  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: 1.6.3
4
+ version: 2.0.0.beta1
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - fujitani sora
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-04-24 00:00:00.000000000 Z
11
+ date: 2026-07-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs
@@ -38,6 +38,34 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '3.4'
41
+ - !ruby/object:Gem::Dependency
42
+ name: language_server-protocol
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.17'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.17'
55
+ - !ruby/object:Gem::Dependency
56
+ name: parallel
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.24'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.24'
41
69
  - !ruby/object:Gem::Dependency
42
70
  name: thor
43
71
  requirement: !ruby/object:Gem::Requirement
@@ -57,6 +85,7 @@ email:
57
85
  - fujitanisora0414@gmail.com
58
86
  executables:
59
87
  - rfmt
88
+ - rfmt-lsp
60
89
  extensions: []
61
90
  extra_rdoc_files: []
62
91
  files:
@@ -64,15 +93,33 @@ files:
64
93
  - LICENSE.txt
65
94
  - README.md
66
95
  - exe/rfmt
96
+ - exe/rfmt-lsp
97
+ - exe/rfmt_fast
98
+ - ext/rfmt/tests/fixtures/parity/comments_mixed.rb
99
+ - ext/rfmt/tests/fixtures/parity/constructs.rb
100
+ - ext/rfmt/tests/fixtures/parity/embdoc.rb
101
+ - ext/rfmt/tests/fixtures/parity/heredoc_assign.rb
102
+ - ext/rfmt/tests/fixtures/parity/heredoc_call_args.rb
103
+ - ext/rfmt/tests/fixtures/parity/metadata_classes.rb
104
+ - ext/rfmt/tests/fixtures/parity/metadata_conditionals.rb
105
+ - ext/rfmt/tests/fixtures/parity/metadata_defs.rb
106
+ - ext/rfmt/tests/fixtures/parity/multibyte.rb
107
+ - ext/rfmt/tests/fixtures/parity/numeric.rb
108
+ - ext/rfmt/tests/fixtures/parity/plain.rb
67
109
  - lib/rfmt.rb
68
110
  - lib/rfmt/3.3/rfmt.so
69
111
  - lib/rfmt/3.4/rfmt.so
70
112
  - lib/rfmt/cache.rb
71
113
  - lib/rfmt/cli.rb
72
114
  - lib/rfmt/configuration.rb
115
+ - lib/rfmt/lsp.rb
116
+ - lib/rfmt/lsp/document_store.rb
117
+ - lib/rfmt/lsp/formatter.rb
118
+ - lib/rfmt/lsp/message_io.rb
119
+ - lib/rfmt/lsp/server.rb
120
+ - lib/rfmt/lsp/uri.rb
121
+ - lib/rfmt/lsp/workspace.rb
73
122
  - lib/rfmt/native_extension_loader.rb
74
- - lib/rfmt/prism_bridge.rb
75
- - lib/rfmt/prism_node_extractor.rb
76
123
  - lib/rfmt/version.rb
77
124
  - lib/ruby_lsp/rfmt/addon.rb
78
125
  - lib/ruby_lsp/rfmt/formatter_runner.rb