ruby-lsp 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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