rucoa 0.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.
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parser/current'
4
+
5
+ module Rucoa
6
+ # Parses Ruby text code.
7
+ class Parser
8
+ class << self
9
+ # @param text [String]
10
+ # @return [Rucoa::Nodes::Base]
11
+ def call(text)
12
+ new(text).call
13
+ end
14
+ end
15
+
16
+ # @param text [String]
17
+ def initialize(text)
18
+ @text = text
19
+ end
20
+
21
+ # @return [Rucoa::Nodes::Base]
22
+ def call
23
+ parser.parse(
24
+ ::Parser::Source::Buffer.new(
25
+ '',
26
+ source: @text
27
+ )
28
+ )
29
+ end
30
+
31
+ private
32
+
33
+ # @return [Parser::Base]
34
+ def parser
35
+ parser = ::Parser::CurrentRuby.new(ParserBuilder.new)
36
+ parser.diagnostics.all_errors_are_fatal = true
37
+ parser.diagnostics.ignore_warnings = true
38
+ parser
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parser/current'
4
+
5
+ module Rucoa
6
+ class ParserBuilder < ::Parser::Builders::Default
7
+ NODE_CLASS_BY_TYPE = {
8
+ str: Nodes::StrNode
9
+ }.freeze
10
+
11
+ # @note Override.
12
+ def n(type, children, source_map)
13
+ node_class_for(type).new(
14
+ type,
15
+ children,
16
+ location: source_map
17
+ )
18
+ end
19
+
20
+ private
21
+
22
+ # @param type [Symbol]
23
+ # @return [Class]
24
+ def node_class_for(type)
25
+ NODE_CLASS_BY_TYPE.fetch(type, Nodes::Base)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rucoa
4
+ class Position
5
+ class << self
6
+ # @param range [Parser::Source::Range]
7
+ # @return [Rucoa::Position]
8
+ def from_parser_range_beginning(range)
9
+ new(
10
+ column: range.column,
11
+ line: range.line
12
+ )
13
+ end
14
+
15
+ # @param range [Parser::Source::Range]
16
+ # @return [Rucoa::Position]
17
+ def from_parser_range_ending(range)
18
+ new(
19
+ column: range.last_column,
20
+ line: range.last_line
21
+ )
22
+ end
23
+
24
+ # @param hash [Hash{Symbol => Integer}]
25
+ # @return [Rucoa::Position]
26
+ def from_vscode_position(hash)
27
+ new(
28
+ column: hash['character'],
29
+ line: hash['line'] + 1
30
+ )
31
+ end
32
+ end
33
+
34
+ # @return [Integer]
35
+ attr_reader :column
36
+
37
+ # @return [Integer]
38
+ attr_reader :line
39
+
40
+ # @param column [Integer] 0-origin column number
41
+ # @param line [Integer] 1-origin line number
42
+ def initialize(column:, line:)
43
+ @column = column
44
+ @line = line
45
+ end
46
+
47
+ # @return [Hash]
48
+ def to_vscode_position
49
+ {
50
+ character: @column,
51
+ line: @line - 1
52
+ }
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rucoa
4
+ class Range
5
+ class << self
6
+ # @param range [Parser::Source::Range]
7
+ # @return [Rucoa::Range]
8
+ def from_parser_range(range)
9
+ new(
10
+ Position.from_parser_range_beginning(range.begin),
11
+ Position.from_parser_range_beginning(range.end)
12
+ )
13
+ end
14
+
15
+ # @param offense [RuboCop::Cop::Offense]
16
+ # @return [Rucoa::Range]
17
+ def from_rubocop_offense(offense)
18
+ new(
19
+ Position.from_parser_range_beginning(offense.location),
20
+ Position.from_parser_range_ending(offense.location)
21
+ )
22
+ end
23
+ end
24
+
25
+ # @param beginning [Rucoa::Position]
26
+ # @param ending [Ruoca::Position]
27
+ def initialize(beginning, ending)
28
+ @beginning = beginning
29
+ @ending = ending
30
+ end
31
+
32
+ # @param position [Rucoa::Position]
33
+ # @return [Boolean]
34
+ def include?(position)
35
+ !exclude?(position)
36
+ end
37
+
38
+ # @return [Hash]
39
+ def to_vscode_range
40
+ {
41
+ end: @ending.to_vscode_position,
42
+ start: @beginning.to_vscode_position
43
+ }
44
+ end
45
+
46
+ private
47
+
48
+ # @param position [Rucoa::Position]
49
+ # @return [Boolean]
50
+ def exclude?(position)
51
+ position.line > @ending.line ||
52
+ position.line < @beginning.line ||
53
+ (position.line == @beginning.line && position.column < @beginning.column) ||
54
+ (position.line == @ending.line && position.column >= @ending.column)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module Rucoa
6
+ class RubocopRunner < ::RuboCop::Runner
7
+ class << self
8
+ # @param path [String]
9
+ # @return [Array<RuboCop::Cop::Offense>]
10
+ def call(path:)
11
+ new(path: path).call
12
+ end
13
+ end
14
+
15
+ # @param path [String]
16
+ def initialize(path:)
17
+ @path = path
18
+ @offenses = []
19
+ super(
20
+ ::RuboCop::Options.new.parse(
21
+ %w[
22
+ --stderr
23
+ --force-exclusion
24
+ --format RuboCop::Formatter::BaseFormatter
25
+ ]
26
+ ).first,
27
+ ::RuboCop::ConfigStore.new
28
+ )
29
+ end
30
+
31
+ # @return [Array<RuboCop::Cop::Offense>]
32
+ def call
33
+ run([@path])
34
+ @offenses
35
+ end
36
+
37
+ private
38
+
39
+ # @param file [String]
40
+ # @param offenses [Array<RuboCop::Cop::Offense>]
41
+ # @return [void]
42
+ def file_finished(file, offenses)
43
+ @offenses = offenses
44
+ super
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rucoa
4
+ class SelectionRangeProvider
5
+ class << self
6
+ # @param source [Rucoa::Source]
7
+ # @param position [Rucoa::Position]
8
+ # @return [Hash, nil]
9
+ def call(position:, source:)
10
+ new(
11
+ position: position,
12
+ source: source
13
+ ).call
14
+ end
15
+ end
16
+
17
+ # @param position [Rucoa::Position]
18
+ # @param source [Rucoa::Source]
19
+ def initialize(position:, source:)
20
+ @position = position
21
+ @source = source
22
+ end
23
+
24
+ # @return [Hash, nil]
25
+ def call
26
+ ranges.reverse.reduce(nil) do |result, range|
27
+ {
28
+ parent: result,
29
+ range: range.to_vscode_range
30
+ }
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ # @return [Rucoa::Nodes::Base, nil]
37
+ def node_at_position
38
+ if instance_variable_defined?(:@node_at_position)
39
+ @node_at_position
40
+ else
41
+ @node_at_position = @source.node_at(@position)
42
+ end
43
+ end
44
+
45
+ # @return [Array<Rucoa::Range>]
46
+ def ranges
47
+ return [] unless node_at_position
48
+
49
+ [node_at_position, *node_at_position.ancestors].flat_map do |node|
50
+ NodeToRangesMapper.call(node)
51
+ end
52
+ end
53
+
54
+ class NodeToRangesMapper
55
+ class << self
56
+ # @param node [Rucoa::Nodes::Base]
57
+ # @return [Array<Rucoa::Range>]
58
+ def call(node)
59
+ new(node).call
60
+ end
61
+ end
62
+
63
+ # @param node [Rucoa::Nodes::Base]
64
+ def initialize(node)
65
+ @node = node
66
+ end
67
+
68
+ # @return [Array<Rucoa::Range>]
69
+ def call
70
+ case @node
71
+ when Nodes::StrNode
72
+ [
73
+ inner_range,
74
+ expression_range
75
+ ]
76
+ else
77
+ []
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ # @return [Rucoa::Range]
84
+ def inner_range
85
+ Range.new(
86
+ Position.from_parser_range_ending(@node.location.begin),
87
+ Position.from_parser_range_beginning(@node.location.end)
88
+ )
89
+ end
90
+
91
+ # @return [Rucoa::Range]
92
+ def expression_range
93
+ Range.from_parser_range(@node.location.expression)
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rucoa
4
+ class Server
5
+ # @param reader [IO]
6
+ # @param writer [IO]
7
+ def initialize(reader:, writer:)
8
+ @reader = MessageReader.new(reader)
9
+ @writer = MessageWriter.new(writer)
10
+ @source_store = SourceStore.new
11
+ end
12
+
13
+ # @return [void]
14
+ def start
15
+ read do |request|
16
+ result = handle(request)
17
+ if result
18
+ write(
19
+ request: request,
20
+ result: result
21
+ )
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ # @param request [Hash]
29
+ # @return [Object]
30
+ def handle(request)
31
+ case request['method']
32
+ when 'initialize'
33
+ on_initialize(request)
34
+ when 'textDocument/didChange'
35
+ on_text_document_did_change(request)
36
+ when 'textDocument/didOpen'
37
+ on_text_document_did_open(request)
38
+ when 'textDocument/selectionRange'
39
+ on_text_document_selection_range(request)
40
+ end
41
+ end
42
+
43
+ # @yieldparam request [Hash]
44
+ # @return [void]
45
+ def read(&block)
46
+ @reader.read(&block)
47
+ end
48
+
49
+ # @param request [Hash]
50
+ # @param result [Object]
51
+ # @return [void]
52
+ def write(request:, result:)
53
+ @writer.write(
54
+ {
55
+ id: request['id'],
56
+ result: result
57
+ }
58
+ )
59
+ end
60
+
61
+ # @param uri [String]
62
+ # @return [void]
63
+ def investigate_diagnostics(uri:)
64
+ diagnostics = DiagnosticProvider.call(
65
+ source: @source_store.get(uri)
66
+ )
67
+ return if diagnostics.empty?
68
+
69
+ @writer.write(
70
+ method: 'textDocument/publishDiagnostics',
71
+ params: {
72
+ diagnostics: diagnostics,
73
+ uri: uri
74
+ }
75
+ )
76
+ end
77
+
78
+ # @param _request [Hash]
79
+ # @return [Hash]
80
+ def on_initialize(_request)
81
+ {
82
+ capabilities: {
83
+ textDocumentSync: {
84
+ change: 1, # Full
85
+ openClose: true
86
+ },
87
+ selectionRangeProvider: true
88
+ }
89
+ }
90
+ end
91
+
92
+ # @param request [Hash]
93
+ # @return [Array<Hash>]
94
+ def on_text_document_did_change(request)
95
+ uri = request.dig('params', 'textDocument', 'uri')
96
+ @source_store.set(
97
+ uri,
98
+ request.dig('params', 'contentChanges')[0]['text']
99
+ )
100
+ investigate_diagnostics(uri: uri)
101
+ nil
102
+ end
103
+
104
+ # @param request [Hash]
105
+ # @return [Array<Hash>]
106
+ def on_text_document_did_open(request)
107
+ uri = request.dig('params', 'textDocument', 'uri')
108
+ @source_store.set(
109
+ uri,
110
+ request.dig('params', 'textDocument', 'text')
111
+ )
112
+ investigate_diagnostics(uri: uri)
113
+ nil
114
+ end
115
+
116
+ # @param request [Hash]
117
+ # @return [Array<Hash>, nil]
118
+ def on_text_document_selection_range(request)
119
+ source = @source_store.get(
120
+ request.dig('params', 'textDocument', 'uri')
121
+ )
122
+ return unless source
123
+
124
+ request.dig('params', 'positions').filter_map do |position|
125
+ SelectionRangeProvider.call(
126
+ position: Position.from_vscode_position(position),
127
+ source: source
128
+ )
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rucoa
4
+ class Source
5
+ # @return [String]
6
+ attr_reader :content
7
+
8
+ # @return [String]
9
+ attr_reader :path
10
+
11
+ # @param content [String]
12
+ # @param path [String, nil]
13
+ def initialize(content:, path: nil)
14
+ @content = content
15
+ @path = path
16
+ end
17
+
18
+ # @param position [Rucoa::Position]
19
+ # @return [Rucoa::Nodes::Base, nil]
20
+ def node_at(position)
21
+ root_and_descendant_nodes.reverse.find do |node|
22
+ node.include_position?(position)
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ # @return [Rucoa::Nodes::Base, nil]
29
+ def root_node
30
+ @root_node ||= Parser.call(@content)
31
+ end
32
+
33
+ # @return [Array<Rucoa::Nodes::Base>]
34
+ def root_and_descendant_nodes
35
+ return [] unless root_node
36
+
37
+ [root_node, *root_node.descendants]
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cgi'
4
+ require 'uri'
5
+
6
+ module Rucoa
7
+ class SourceStore
8
+ def initialize
9
+ @data = {}
10
+ end
11
+
12
+ # @param uri [String]
13
+ # @return [String, nil]
14
+ def get(uri)
15
+ @data[uri]
16
+ end
17
+
18
+ # @param uri [String]
19
+ # @param content [String]
20
+ # @return [void]
21
+ def set(uri, content)
22
+ @data[uri] = Source.new(
23
+ content: content,
24
+ path: path_from_uri(uri)
25
+ )
26
+ end
27
+
28
+ private
29
+
30
+ # @param uri [String]
31
+ # @return [String]
32
+ def path_from_uri(uri)
33
+ ::CGI.unescape(
34
+ ::URI.parse(uri).path
35
+ )
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rucoa
4
+ VERSION = '0.1.0'
5
+ end
data/lib/rucoa.rb ADDED
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'rucoa/version'
4
+
5
+ module Rucoa
6
+ autoload :Cli, 'rucoa/cli'
7
+ autoload :Errors, 'rucoa/errors'
8
+ autoload :DiagnosticProvider, 'rucoa/diagnostic_provider'
9
+ autoload :MessageReader, 'rucoa/message_reader'
10
+ autoload :MessageWriter, 'rucoa/message_writer'
11
+ autoload :Nodes, 'rucoa/nodes'
12
+ autoload :ParserBuilder, 'rucoa/parser_builder'
13
+ autoload :Parser, 'rucoa/parser'
14
+ autoload :Position, 'rucoa/position'
15
+ autoload :Range, 'rucoa/range'
16
+ autoload :RubocopRunner, 'rucoa/rubocop_runner'
17
+ autoload :SelectionRangeProvider, 'rucoa/selection_range_provider'
18
+ autoload :Server, 'rucoa/server'
19
+ autoload :Source, 'rucoa/source'
20
+ autoload :SourceStore, 'rucoa/source_store'
21
+ end
data/rucoa.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/rucoa/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'rucoa'
7
+ spec.version = Rucoa::VERSION
8
+ spec.authors = ['Ryo Nakamura']
9
+ spec.email = ['r7kamura@gmail.com']
10
+
11
+ spec.summary = 'Language server for Ruby.'
12
+ spec.homepage = 'https://github.com/r7kamura/rucoa'
13
+ spec.license = 'MIT'
14
+ spec.required_ruby_version = '>= 2.7'
15
+
16
+ spec.metadata['homepage_uri'] = spec.homepage
17
+ spec.metadata['source_code_uri'] = spec.homepage
18
+ spec.metadata['changelog_uri'] = "#{spec.homepage}/releases"
19
+ spec.metadata['rubygems_mfa_required'] = 'true'
20
+
21
+ spec.files = Dir.chdir(__dir__) do
22
+ `git ls-files -z`.split("\x0").reject do |f|
23
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
24
+ end
25
+ end
26
+ spec.bindir = 'exe'
27
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ['lib']
29
+
30
+ spec.add_dependency 'parser'
31
+ spec.add_dependency 'rubocop'
32
+ end
metadata ADDED
@@ -0,0 +1,105 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rucoa
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ryo Nakamura
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-09-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: parser
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubocop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description:
42
+ email:
43
+ - r7kamura@gmail.com
44
+ executables:
45
+ - rucoa
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - ".rspec"
50
+ - ".rubocop.yml"
51
+ - CODE_OF_CONDUCT.md
52
+ - Gemfile
53
+ - Gemfile.lock
54
+ - LICENSE.txt
55
+ - README.md
56
+ - Rakefile
57
+ - exe/rucoa
58
+ - lib/rucoa.rb
59
+ - lib/rucoa/cli.rb
60
+ - lib/rucoa/diagnostic_provider.rb
61
+ - lib/rucoa/errors.rb
62
+ - lib/rucoa/message_reader.rb
63
+ - lib/rucoa/message_writer.rb
64
+ - lib/rucoa/nodes.rb
65
+ - lib/rucoa/nodes/base.rb
66
+ - lib/rucoa/nodes/str_node.rb
67
+ - lib/rucoa/parser.rb
68
+ - lib/rucoa/parser_builder.rb
69
+ - lib/rucoa/position.rb
70
+ - lib/rucoa/range.rb
71
+ - lib/rucoa/rubocop_runner.rb
72
+ - lib/rucoa/selection_range_provider.rb
73
+ - lib/rucoa/server.rb
74
+ - lib/rucoa/source.rb
75
+ - lib/rucoa/source_store.rb
76
+ - lib/rucoa/version.rb
77
+ - rucoa.gemspec
78
+ homepage: https://github.com/r7kamura/rucoa
79
+ licenses:
80
+ - MIT
81
+ metadata:
82
+ homepage_uri: https://github.com/r7kamura/rucoa
83
+ source_code_uri: https://github.com/r7kamura/rucoa
84
+ changelog_uri: https://github.com/r7kamura/rucoa/releases
85
+ rubygems_mfa_required: 'true'
86
+ post_install_message:
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '2.7'
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ requirements: []
101
+ rubygems_version: 3.3.7
102
+ signing_key:
103
+ specification_version: 4
104
+ summary: Language server for Ruby.
105
+ test_files: []