ruby-lsp 0.0.1 → 0.0.2

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,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLsp
4
+ module Requests
5
+ class Diagnostics < RuboCopRequest
6
+ RUBOCOP_TO_LSP_SEVERITY = {
7
+ convention: LanguageServer::Protocol::Constant::DiagnosticSeverity::INFORMATION,
8
+ info: LanguageServer::Protocol::Constant::DiagnosticSeverity::INFORMATION,
9
+ refactor: LanguageServer::Protocol::Constant::DiagnosticSeverity::INFORMATION,
10
+ warning: LanguageServer::Protocol::Constant::DiagnosticSeverity::WARNING,
11
+ error: LanguageServer::Protocol::Constant::DiagnosticSeverity::ERROR,
12
+ fatal: LanguageServer::Protocol::Constant::DiagnosticSeverity::ERROR,
13
+ }.freeze
14
+
15
+ def run
16
+ super
17
+
18
+ @diagnostics
19
+ end
20
+
21
+ def file_finished(_file, offenses)
22
+ @diagnostics = offenses.map { |offense| Diagnostic.new(offense, @uri) }
23
+ end
24
+
25
+ class Diagnostic
26
+ attr_reader :replacements
27
+
28
+ def initialize(offense, uri)
29
+ @offense = offense
30
+ @uri = uri
31
+ @replacements = offense.correctable? ? offense_replacements : []
32
+ end
33
+
34
+ def correctable?
35
+ @offense.correctable?
36
+ end
37
+
38
+ def in_range?(range)
39
+ range.cover?(@offense.line - 1)
40
+ end
41
+
42
+ def to_lsp_code_action
43
+ LanguageServer::Protocol::Interface::CodeAction.new(
44
+ title: "Autocorrect #{@offense.cop_name}",
45
+ kind: LanguageServer::Protocol::Constant::CodeActionKind::QUICK_FIX,
46
+ edit: LanguageServer::Protocol::Interface::WorkspaceEdit.new(
47
+ document_changes: [
48
+ LanguageServer::Protocol::Interface::TextDocumentEdit.new(
49
+ text_document: LanguageServer::Protocol::Interface::OptionalVersionedTextDocumentIdentifier.new(
50
+ uri: @uri,
51
+ version: nil
52
+ ),
53
+ edits: @replacements
54
+ ),
55
+ ]
56
+ ),
57
+ is_preferred: true,
58
+ )
59
+ end
60
+
61
+ def to_lsp_diagnostic
62
+ LanguageServer::Protocol::Interface::Diagnostic.new(
63
+ message: @offense.message,
64
+ source: "RuboCop",
65
+ code: @offense.cop_name,
66
+ severity: RUBOCOP_TO_LSP_SEVERITY[@offense.severity.name],
67
+ range: LanguageServer::Protocol::Interface::Range.new(
68
+ start: LanguageServer::Protocol::Interface::Position.new(
69
+ line: @offense.line - 1,
70
+ character: @offense.column
71
+ ),
72
+ end: LanguageServer::Protocol::Interface::Position.new(
73
+ line: @offense.last_line - 1,
74
+ character: @offense.last_column
75
+ )
76
+ )
77
+ )
78
+ end
79
+
80
+ private
81
+
82
+ def offense_replacements
83
+ @offense.corrector.as_replacements.map do |range, replacement|
84
+ LanguageServer::Protocol::Interface::TextEdit.new(
85
+ range: LanguageServer::Protocol::Interface::Range.new(
86
+ start: LanguageServer::Protocol::Interface::Position.new(line: range.line - 1, character: range.column),
87
+ end: LanguageServer::Protocol::Interface::Position.new(line: range.last_line - 1,
88
+ character: range.last_column)
89
+ ),
90
+ new_text: replacement
91
+ )
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLsp
4
+ module Requests
5
+ class DocumentSymbol < BaseRequest
6
+ SYMBOL_KIND = {
7
+ file: 1,
8
+ module: 2,
9
+ namespace: 3,
10
+ package: 4,
11
+ class: 5,
12
+ method: 6,
13
+ property: 7,
14
+ field: 8,
15
+ constructor: 9,
16
+ enum: 10,
17
+ interface: 11,
18
+ function: 12,
19
+ variable: 13,
20
+ constant: 14,
21
+ string: 15,
22
+ number: 16,
23
+ boolean: 17,
24
+ array: 18,
25
+ object: 19,
26
+ key: 20,
27
+ null: 21,
28
+ enummember: 22,
29
+ struct: 23,
30
+ event: 24,
31
+ operator: 25,
32
+ typeparameter: 26,
33
+ }.freeze
34
+
35
+ ATTR_ACCESSORS = ["attr_reader", "attr_writer", "attr_accessor"].freeze
36
+
37
+ class SymbolHierarchyRoot
38
+ attr_reader :children
39
+
40
+ def initialize
41
+ @children = []
42
+ end
43
+ end
44
+
45
+ def initialize(document)
46
+ super
47
+
48
+ @root = SymbolHierarchyRoot.new
49
+ @stack = [@root]
50
+ end
51
+
52
+ def run
53
+ visit(@document.tree)
54
+ @root.children
55
+ end
56
+
57
+ def visit_class(node)
58
+ symbol = create_document_symbol(
59
+ name: node.constant.constant.value,
60
+ kind: :class,
61
+ range_node: node,
62
+ selection_range_node: node.constant
63
+ )
64
+
65
+ @stack << symbol
66
+ visit(node.bodystmt)
67
+ @stack.pop
68
+ end
69
+
70
+ def visit_command(node)
71
+ return unless ATTR_ACCESSORS.include?(node.message.value)
72
+
73
+ node.arguments.parts.each do |argument|
74
+ next unless argument.is_a?(SyntaxTree::SymbolLiteral)
75
+
76
+ create_document_symbol(
77
+ name: argument.value.value,
78
+ kind: :field,
79
+ range_node: argument,
80
+ selection_range_node: argument.value
81
+ )
82
+ end
83
+ end
84
+
85
+ def visit_const_path_field(node)
86
+ create_document_symbol(
87
+ name: node.constant.value,
88
+ kind: :constant,
89
+ range_node: node,
90
+ selection_range_node: node.constant
91
+ )
92
+ end
93
+
94
+ def visit_def(node)
95
+ name = node.name.value
96
+
97
+ symbol = create_document_symbol(
98
+ name: name,
99
+ kind: name == "initialize" ? :constructor : :method,
100
+ range_node: node,
101
+ selection_range_node: node.name
102
+ )
103
+
104
+ @stack << symbol
105
+ visit(node.bodystmt)
106
+ @stack.pop
107
+ end
108
+
109
+ def visit_def_endless(node)
110
+ name = node.name.value
111
+
112
+ symbol = create_document_symbol(
113
+ name: name,
114
+ kind: name == "initialize" ? :constructor : :method,
115
+ range_node: node,
116
+ selection_range_node: node.name
117
+ )
118
+
119
+ @stack << symbol
120
+ visit(node.statement)
121
+ @stack.pop
122
+ end
123
+
124
+ def visit_defs(node)
125
+ symbol = create_document_symbol(
126
+ name: "self.#{node.name.value}",
127
+ kind: :method,
128
+ range_node: node,
129
+ selection_range_node: node.name
130
+ )
131
+
132
+ @stack << symbol
133
+ visit(node.bodystmt)
134
+ @stack.pop
135
+ end
136
+
137
+ def visit_module(node)
138
+ symbol = create_document_symbol(
139
+ name: node.constant.constant.value,
140
+ kind: :module,
141
+ range_node: node,
142
+ selection_range_node: node.constant
143
+ )
144
+
145
+ @stack << symbol
146
+ visit(node.bodystmt)
147
+ @stack.pop
148
+ end
149
+
150
+ def visit_top_const_field(node)
151
+ create_document_symbol(
152
+ name: node.constant.value,
153
+ kind: :constant,
154
+ range_node: node,
155
+ selection_range_node: node.constant
156
+ )
157
+ end
158
+
159
+ def visit_var_field(node)
160
+ kind = case node.value
161
+ when SyntaxTree::Const
162
+ :constant
163
+ when SyntaxTree::CVar, SyntaxTree::IVar
164
+ :variable
165
+ else
166
+ return
167
+ end
168
+
169
+ create_document_symbol(
170
+ name: node.value.value,
171
+ kind: kind,
172
+ range_node: node,
173
+ selection_range_node: node.value
174
+ )
175
+ end
176
+
177
+ private
178
+
179
+ def create_document_symbol(name:, kind:, range_node:, selection_range_node:)
180
+ symbol = LanguageServer::Protocol::Interface::DocumentSymbol.new(
181
+ name: name,
182
+ kind: SYMBOL_KIND[kind],
183
+ range: range_from_syntax_tree_node(range_node),
184
+ selection_range: range_from_syntax_tree_node(selection_range_node),
185
+ children: [],
186
+ )
187
+
188
+ @stack.last.children << symbol
189
+
190
+ symbol
191
+ end
192
+
193
+ def range_from_syntax_tree_node(node)
194
+ loc = node.location
195
+
196
+ LanguageServer::Protocol::Interface::Range.new(
197
+ start: LanguageServer::Protocol::Interface::Position.new(line: loc.start_line - 1,
198
+ character: loc.start_column),
199
+ end: LanguageServer::Protocol::Interface::Position.new(line: loc.end_line - 1, character: loc.end_column),
200
+ )
201
+ end
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLsp
4
+ module Requests
5
+ class FoldingRanges < BaseRequest
6
+ SIMPLE_FOLDABLES = [
7
+ SyntaxTree::ArrayLiteral,
8
+ SyntaxTree::BraceBlock,
9
+ SyntaxTree::Case,
10
+ SyntaxTree::ClassDeclaration,
11
+ SyntaxTree::Command,
12
+ SyntaxTree::DoBlock,
13
+ SyntaxTree::FCall,
14
+ SyntaxTree::For,
15
+ SyntaxTree::HashLiteral,
16
+ SyntaxTree::Heredoc,
17
+ SyntaxTree::If,
18
+ SyntaxTree::ModuleDeclaration,
19
+ SyntaxTree::SClass,
20
+ SyntaxTree::Unless,
21
+ SyntaxTree::Until,
22
+ SyntaxTree::While,
23
+ ].freeze
24
+
25
+ NODES_WITH_STATEMENTS = [
26
+ SyntaxTree::Else,
27
+ SyntaxTree::Elsif,
28
+ SyntaxTree::Ensure,
29
+ SyntaxTree::In,
30
+ SyntaxTree::Rescue,
31
+ SyntaxTree::When,
32
+ ].freeze
33
+
34
+ def initialize(document)
35
+ super
36
+
37
+ @ranges = []
38
+ @partial_range = nil
39
+ end
40
+
41
+ def run
42
+ visit(@document.tree)
43
+ emit_partial_range
44
+ @ranges
45
+ end
46
+
47
+ private
48
+
49
+ def visit(node)
50
+ return unless handle_partial_range(node)
51
+
52
+ case node
53
+ when *SIMPLE_FOLDABLES
54
+ add_node_range(node)
55
+ when *NODES_WITH_STATEMENTS
56
+ add_statements_range(node, node.statements)
57
+ when SyntaxTree::Begin
58
+ add_statements_range(node, node.bodystmt.statements)
59
+ when SyntaxTree::Call, SyntaxTree::CommandCall
60
+ add_call_range(node)
61
+ return
62
+ when SyntaxTree::Def, SyntaxTree::Defs
63
+ add_def_range(node)
64
+ when SyntaxTree::StringConcat
65
+ add_string_concat(node)
66
+ return
67
+ end
68
+
69
+ super
70
+ end
71
+
72
+ class PartialRange
73
+ attr_reader :kind, :end_line
74
+
75
+ def self.from(node, kind)
76
+ new(node.location.start_line - 1, node.location.end_line - 1, kind)
77
+ end
78
+
79
+ def initialize(start_line, end_line, kind)
80
+ @start_line = start_line
81
+ @end_line = end_line
82
+ @kind = kind
83
+ end
84
+
85
+ def extend_to(node)
86
+ @end_line = node.location.end_line - 1
87
+ self
88
+ end
89
+
90
+ def new_section?(node)
91
+ node.is_a?(SyntaxTree::Comment) && @end_line + 1 != node.location.start_line - 1
92
+ end
93
+
94
+ def to_range
95
+ LanguageServer::Protocol::Interface::FoldingRange.new(
96
+ start_line: @start_line,
97
+ end_line: @end_line,
98
+ kind: @kind
99
+ )
100
+ end
101
+ end
102
+
103
+ def handle_partial_range(node)
104
+ kind = partial_range_kind(node)
105
+
106
+ if kind.nil?
107
+ emit_partial_range
108
+ return true
109
+ end
110
+
111
+ @partial_range = if @partial_range.nil?
112
+ PartialRange.from(node, kind)
113
+ elsif @partial_range.kind != kind || @partial_range.new_section?(node)
114
+ emit_partial_range
115
+ PartialRange.from(node, kind)
116
+ else
117
+ @partial_range.extend_to(node)
118
+ end
119
+
120
+ false
121
+ end
122
+
123
+ def partial_range_kind(node)
124
+ case node
125
+ when SyntaxTree::Comment
126
+ "comment"
127
+ when SyntaxTree::Command
128
+ if node.message.value == "require" || node.message.value == "require_relative"
129
+ "imports"
130
+ end
131
+ end
132
+ end
133
+
134
+ def emit_partial_range
135
+ return if @partial_range.nil?
136
+
137
+ @ranges << @partial_range.to_range
138
+ @partial_range = nil
139
+ end
140
+
141
+ def add_call_range(node)
142
+ receiver = node.receiver
143
+ loop do
144
+ case receiver
145
+ when SyntaxTree::Call
146
+ visit(receiver.arguments)
147
+ receiver = receiver.receiver
148
+ when SyntaxTree::MethodAddBlock
149
+ visit(receiver.block)
150
+ receiver = receiver.call.receiver
151
+ else
152
+ break
153
+ end
154
+ end
155
+
156
+ add_lines_range(receiver.location.start_line, node.location.end_line)
157
+
158
+ visit(node.arguments)
159
+ end
160
+
161
+ def add_def_range(node)
162
+ params_location = node.params.location
163
+
164
+ if params_location.start_line < params_location.end_line
165
+ add_lines_range(params_location.end_line, node.location.end_line)
166
+ else
167
+ add_node_range(node)
168
+ end
169
+
170
+ visit(node.bodystmt.statements)
171
+ end
172
+
173
+ def add_statements_range(node, statements)
174
+ add_lines_range(node.location.start_line, statements.location.end_line) unless statements.empty?
175
+ end
176
+
177
+ def add_string_concat(node)
178
+ left = node.left
179
+ left = left.left while left.is_a?(SyntaxTree::StringConcat)
180
+
181
+ add_lines_range(left.location.start_line, node.right.location.end_line)
182
+ end
183
+
184
+ def add_node_range(node)
185
+ add_location_range(node.location)
186
+ end
187
+
188
+ def add_location_range(location)
189
+ add_lines_range(location.start_line, location.end_line)
190
+ end
191
+
192
+ def add_lines_range(start_line, end_line)
193
+ return if start_line >= end_line
194
+
195
+ @ranges << LanguageServer::Protocol::Interface::FoldingRange.new(
196
+ start_line: start_line - 1,
197
+ end_line: end_line - 1,
198
+ kind: "region"
199
+ )
200
+ end
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLsp
4
+ module Requests
5
+ class Formatting < RuboCopRequest
6
+ RUBOCOP_FLAGS = (COMMON_RUBOCOP_FLAGS + ["--auto-correct"]).freeze
7
+
8
+ def initialize(uri, document)
9
+ super
10
+ @formatted_text = nil
11
+ end
12
+
13
+ def run
14
+ super
15
+
16
+ @formatted_text = @options[:stdin] # Rubocop applies the corrections on stdin
17
+ return unless @formatted_text
18
+
19
+ [
20
+ LanguageServer::Protocol::Interface::TextEdit.new(
21
+ range: LanguageServer::Protocol::Interface::Range.new(
22
+ start: LanguageServer::Protocol::Interface::Position.new(line: 0, character: 0),
23
+ end: LanguageServer::Protocol::Interface::Position.new(
24
+ line: text.size,
25
+ character: text.size
26
+ )
27
+ ),
28
+ new_text: @formatted_text
29
+ ),
30
+ ]
31
+ end
32
+
33
+ private
34
+
35
+ def rubocop_flags
36
+ RUBOCOP_FLAGS
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+ require "cgi"
5
+
6
+ module RubyLsp
7
+ module Requests
8
+ class RuboCopRequest < RuboCop::Runner
9
+ COMMON_RUBOCOP_FLAGS = [
10
+ "--stderr", # Print any output to stderr so that our stdout does not get polluted
11
+ "--format",
12
+ "RuboCop::Formatter::BaseFormatter", # Suppress any output by using the base formatter
13
+ ].freeze
14
+
15
+ attr_reader :file, :text
16
+
17
+ def self.run(uri, document)
18
+ new(uri, document).run
19
+ end
20
+
21
+ def initialize(uri, document)
22
+ @file = CGI.unescape(URI.parse(uri).path)
23
+ @text = document.source
24
+ @uri = uri
25
+
26
+ super(
27
+ ::RuboCop::Options.new.parse(rubocop_flags).first,
28
+ ::RuboCop::ConfigStore.new
29
+ )
30
+ end
31
+
32
+ def run
33
+ # We communicate with Rubocop via stdin
34
+ @options[:stdin] = text
35
+
36
+ # Invoke the actual run method with just this file in `paths`
37
+ super([file])
38
+ end
39
+
40
+ private
41
+
42
+ def rubocop_flags
43
+ COMMON_RUBOCOP_FLAGS
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLsp
4
+ module Requests
5
+ class SemanticHighlighting < BaseRequest
6
+ TOKEN_TYPES = [
7
+ :variable,
8
+ :method,
9
+ ].freeze
10
+ TOKEN_MODIFIERS = [].freeze
11
+
12
+ def initialize(document)
13
+ super
14
+
15
+ @tokens = []
16
+ @tree = document.tree
17
+ @current_row = 0
18
+ @current_column = 0
19
+ end
20
+
21
+ def run
22
+ visit(@tree)
23
+ LanguageServer::Protocol::Interface::SemanticTokens.new(data: @tokens)
24
+ end
25
+
26
+ def visit_m_assign(node)
27
+ node.target.parts.each do |var_ref|
28
+ add_token(var_ref.value.location, :variable)
29
+ end
30
+ end
31
+
32
+ def visit_var_field(node)
33
+ case node.value
34
+ when SyntaxTree::Ident
35
+ add_token(node.value.location, :variable)
36
+ end
37
+ end
38
+
39
+ def visit_var_ref(node)
40
+ case node.value
41
+ when SyntaxTree::Ident
42
+ add_token(node.value.location, :variable)
43
+ end
44
+ end
45
+
46
+ def visit_a_ref_field(node)
47
+ add_token(node.collection.value.location, :variable)
48
+ end
49
+
50
+ def visit_call(node)
51
+ visit(node.receiver)
52
+ add_token(node.message.location, :method)
53
+ visit(node.arguments)
54
+ end
55
+
56
+ def visit_command(node)
57
+ add_token(node.message.location, :method)
58
+ visit(node.arguments)
59
+ end
60
+
61
+ def visit_command_call(node)
62
+ visit(node.receiver)
63
+ add_token(node.message.location, :method)
64
+ visit(node.arguments)
65
+ end
66
+
67
+ def visit_fcall(node)
68
+ add_token(node.value.location, :method)
69
+ visit(node.arguments)
70
+ end
71
+
72
+ def visit_vcall(node)
73
+ add_token(node.value.location, :method)
74
+ end
75
+
76
+ def add_token(location, classification)
77
+ length = location.end_char - location.start_char
78
+
79
+ compute_delta(location) do |delta_line, delta_column|
80
+ @tokens.push(delta_line, delta_column, length, TOKEN_TYPES.index(classification), 0)
81
+ end
82
+ end
83
+
84
+ # The delta array is computed according to the LSP specification:
85
+ # > The protocol for the token format relative uses relative
86
+ # > positions, because most tokens remain stable relative to
87
+ # > each other when edits are made in a file. This simplifies
88
+ # > the computation of a delta if a server supports it. So each
89
+ # > token is represented using 5 integers.
90
+
91
+ # For more information on how each number is calculated, read:
92
+ # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_semanticTokens
93
+ def compute_delta(location)
94
+ row = location.start_line - 1
95
+ column = location.start_column
96
+
97
+ if row < @current_row
98
+ raise InvalidTokenRowError, "Invalid token row detected: " \
99
+ "Ensure tokens are added in the expected order."
100
+ end
101
+
102
+ delta_line = row - @current_row
103
+
104
+ delta_column = column
105
+ delta_column -= @current_column if delta_line == 0
106
+
107
+ yield delta_line, delta_column
108
+
109
+ @current_row = row
110
+ @current_column = column
111
+ end
112
+
113
+ class InvalidTokenRowError < StandardError; end
114
+ end
115
+ end
116
+ end