ruby-lsp 0.2.4 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f263a066e68d798f4808df8b595c06345061623ab706b9f3ed8ffa0573d8ea47
4
- data.tar.gz: 46eeeb5c2938d46e24a2d0a01e5772a49c27db9f3ab4a03ad840a1059e5161c7
3
+ metadata.gz: 3e09a85000549c0b7b7dd2886b49d44ca2e32b830616a4af7181a2d7952f948f
4
+ data.tar.gz: f19b017ebffce3a090d9fe9c39c94cb0dad75a7a74856c099443839524d513f3
5
5
  SHA512:
6
- metadata.gz: e9c78db0a6b5262fc618fd1e7d4832f7a74d76130a29767a339897a06b5ff6fbc2799b328127f596d327d0adbff074e17ebacc5b88a7af97dff640220466ce1b
7
- data.tar.gz: 3ddc525a0ad7220537db5ee0a94a208182821d0f5c321f5ab3babafba97d84ee196b392cedc7c925be20df801244c519d425822941e791a73563224f0b00b6b6
6
+ metadata.gz: e0b75fa95c43a4a15a46e3cd663b5dd637c9288497116b356eb69dc3f831cfdbb3e167c1c578b68feee715e85c72d26c7d63236e3250552ddbf0f2db334e8f46
7
+ data.tar.gz: c80518dba77375f07f9944f0401f6836ca69bba60ba6723677e33e366104dd69b70832e8ce76c05312b47c3a032a5308711505e88be2ce69f3f837bec2db3c21
data/CHANGELOG.md CHANGED
@@ -6,32 +6,29 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
- ## [0.2.3]
10
-
11
- - Resolve generic source URIs for jump to gem source (https://github.com/Shopify/ruby-lsp/pull/237)
12
-
13
- ## [0.2.2]
14
-
15
- - Support document links (https://github.com/Shopify/ruby-lsp/pull/195)
16
- - Avoid returning on request blocks (https://github.com/Shopify/ruby-lsp/pull/232)
17
- - Better specify gemspec files (https://github.com/Shopify/ruby-lsp/pull/233)
18
- - Include Kernel instance methods as special methods for semantic highlighting (https://github.com/Shopify/ruby-lsp/pull/231)
19
- - Fix call processing when message is a :call symbol literal (https://github.com/Shopify/ruby-lsp/pull/236)
20
- - Alert users about non auto-correctable diagnostics (https://github.com/Shopify/ruby-lsp/pull/230)
21
- - Let clients pull diagnostics instead of pushing on edits (https://github.com/Shopify/ruby-lsp/pull/242)
22
-
23
- ## [0.2.1]
24
-
25
- - Implement the exit lifecycle request (https://github.com/Shopify/ruby-lsp/pull/198)
26
- - Remove the Sorbet runtime from the gem's default load path (https://github.com/Shopify/ruby-lsp/pull/214)
27
- - Return nil if the document is already formatted (https://github.com/Shopify/ruby-lsp/pull/216)
28
- - Handle nameless keyword rest parameters in semantic highlighting (https://github.com/Shopify/ruby-lsp/pull/222)
29
- - Display a warning on invalid RuboCop configuration (https://github.com/Shopify/ruby-lsp/pull/226)
30
- - Centralize request handling logic in server.rb (https://github.com/Shopify/ruby-lsp/pull/221)
31
- - Fix folding ranges for chained invocations involving an FCall (https://github.com/Shopify/ruby-lsp/pull/223)
32
- - Fix handling of argument fowarding in semantic highlighting (https://github.com/Shopify/ruby-lsp/pull/228)
33
- - Recover from initial syntax errors when opening documents (https://github.com/Shopify/ruby-lsp/pull/224)
34
- - Highlight occurrences and definitions in document highlight (https://github.com/Shopify/ruby-lsp/pull/187)
9
+ ## [0.3.2]
10
+
11
+ - Make the diagnostic request parallel (https://github.com/Shopify/ruby-lsp/pull/293)
12
+ - Improve worker stability (https://github.com/Shopify/ruby-lsp/pull/295)
13
+
14
+ ## [0.3.1]
15
+
16
+ - Resolve TODO for LSP v3.17 (https://github.com/Shopify/ruby-lsp/pull/268)
17
+ - Add dependency constraint for LSP v3.17 (https://github.com/Shopify/ruby-lsp/pull/269)
18
+ - Handle class/module declarations as a class token with declaration modifier (https://github.com/Shopify/ruby-lsp/pull/260)
19
+ - Handle required parameters in semantic highlighting (https://github.com/Shopify/ruby-lsp/pull/271)
20
+ - Add comment continuation via on type on_type_formatting (https://github.com/Shopify/ruby-lsp/pull/274)
21
+ - Make RuboCop runner use composition instead of inheritance (https://github.com/Shopify/ruby-lsp/pull/278)
22
+ - Protect worker against cancellation during popping (https://github.com/Shopify/ruby-lsp/pull/280)
23
+ - Handle formatting errors in on_error block (https://github.com/Shopify/ruby-lsp/pull/279)
24
+ - Fix on type formatting pipe completion for regular or expressions (https://github.com/Shopify/ruby-lsp/pull/282)
25
+ - Do not fail on LoadError (https://github.com/Shopify/ruby-lsp/pull/292)
26
+
27
+ ## [0.3.0]
28
+ - Add on type formatting completions (https://github.com/Shopify/ruby-lsp/pull/253)
29
+ - Upgrade syntax_tree requirement to >= 3.4 (https://github.com/Shopify/ruby-lsp/pull/254)
30
+ - Show error message when there's a InfiniteCorrectionLoop exception (https://github.com/Shopify/ruby-lsp/pull/252)
31
+ - Add request cancellation (https://github.com/Shopify/ruby-lsp/pull/243)
35
32
 
36
33
  ## [0.2.0]
37
34
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.4
1
+ 0.3.2
@@ -3,7 +3,7 @@
3
3
 
4
4
  require "ruby_lsp/requests"
5
5
  require "ruby_lsp/store"
6
- require "benchmark"
6
+ require "ruby_lsp/queue"
7
7
 
8
8
  module RubyLsp
9
9
  Interface = LanguageServer::Protocol::Interface
@@ -14,11 +14,35 @@ module RubyLsp
14
14
  extend T::Sig
15
15
  VOID = T.let(Object.new.freeze, Object)
16
16
 
17
- sig { params(blk: T.proc.bind(Handler).params(arg0: T.untyped).void).void }
18
- def self.start(&blk)
19
- handler = new
20
- handler.instance_exec(&blk)
21
- handler.start
17
+ class RequestHandler < T::Struct
18
+ extend T::Sig
19
+
20
+ const :action, T.proc.params(request: T::Hash[Symbol, T.untyped]).returns(T.untyped)
21
+ const :parallel, T::Boolean
22
+ prop :error_handler,
23
+ T.nilable(T.proc.params(error: Exception, request: T::Hash[Symbol, T.untyped]).void)
24
+
25
+ # A proc that runs in case a request has errored. Receives the error and the original request as arguments. Useful
26
+ # for displaying window messages on errors
27
+ sig do
28
+ params(
29
+ block: T.proc.bind(Handler).params(error: Exception, request: T::Hash[Symbol, T.untyped]).void
30
+ ).void
31
+ end
32
+ def on_error(&block)
33
+ self.error_handler = block
34
+ end
35
+ end
36
+
37
+ class << self
38
+ extend T::Sig
39
+
40
+ sig { params(blk: T.proc.bind(Handler).params(arg0: T.untyped).void).void }
41
+ def start(&blk)
42
+ handler = new
43
+ handler.instance_exec(&blk)
44
+ handler.start
45
+ end
22
46
  end
23
47
 
24
48
  sig { returns(Store) }
@@ -28,61 +52,50 @@ module RubyLsp
28
52
  def initialize
29
53
  @writer = T.let(Transport::Stdio::Writer.new, Transport::Stdio::Writer)
30
54
  @reader = T.let(Transport::Stdio::Reader.new, Transport::Stdio::Reader)
31
- @handlers = T.let({}, T::Hash[String, T.proc.params(request: T::Hash[Symbol, T.untyped]).returns(T.untyped)])
55
+ @handlers = T.let({}, T::Hash[String, RequestHandler])
32
56
  @store = T.let(Store.new, Store)
57
+ @queue = T.let(Queue.new(@writer, @handlers), Queue)
33
58
  end
34
59
 
35
60
  sig { void }
36
61
  def start
37
62
  $stderr.puts "Starting Ruby LSP..."
38
- @reader.read { |request| handle(request) }
63
+
64
+ @reader.read do |request|
65
+ handler = @handlers[request[:method]]
66
+ next if handler.nil?
67
+
68
+ if handler.parallel
69
+ @queue.push(request)
70
+ else
71
+ result = @queue.execute(request)
72
+ @queue.finalize_request(result, request)
73
+ end
74
+ end
39
75
  end
40
76
 
41
77
  private
42
78
 
79
+ sig { params(id: T.any(String, Integer)).void }
80
+ def cancel_request(id)
81
+ @queue.cancel(id)
82
+ end
83
+
43
84
  sig do
44
85
  params(
45
86
  msg: String,
87
+ parallel: T::Boolean,
46
88
  blk: T.proc.bind(Handler).params(request: T::Hash[Symbol, T.untyped]).returns(T.untyped)
47
- ).void
89
+ ).returns(RequestHandler)
48
90
  end
49
- def on(msg, &blk)
50
- @handlers[msg] = blk
51
- end
52
-
53
- sig { params(request: T::Hash[Symbol, T.untyped]).void }
54
- def handle(request)
55
- result = T.let(nil, T.untyped)
56
- error = T.let(nil, T.nilable(StandardError))
57
- handler = @handlers[request[:method]]
58
-
59
- request_time = Benchmark.realtime do
60
- if handler
61
- begin
62
- result = handler.call(request)
63
- rescue StandardError => e
64
- error = e
65
- end
66
-
67
- if error
68
- @writer.write(
69
- {
70
- id: request[:id],
71
- error: { code: Constant::ErrorCodes::INTERNAL_ERROR, message: error.inspect, data: request.to_json },
72
- }
73
- )
74
- elsif result != VOID
75
- @writer.write(id: request[:id], result: result)
76
- end
77
- end
78
- end
79
-
80
- @writer.write(method: "telemetry/event", params: telemetry_params(request, request_time, error))
91
+ def on(msg, parallel: false, &blk)
92
+ @handlers[msg] = RequestHandler.new(action: blk, parallel: parallel)
81
93
  end
82
94
 
83
95
  sig { void }
84
96
  def shutdown
85
97
  $stderr.puts "Shutting down Ruby LSP..."
98
+ @queue.shutdown
86
99
  store.clear
87
100
  end
88
101
 
@@ -101,30 +114,5 @@ module RubyLsp
101
114
  params: Interface::ShowMessageParams.new(type: type, message: message)
102
115
  )
103
116
  end
104
-
105
- sig do
106
- params(
107
- request: T::Hash[Symbol, T.untyped],
108
- request_time: Float,
109
- error: T.nilable(StandardError)
110
- ).returns(T::Hash[Symbol, T.any(String, Float)])
111
- end
112
- def telemetry_params(request, request_time, error)
113
- uri = request.dig(:params, :textDocument, :uri)
114
-
115
- params = {
116
- request: request[:method],
117
- lspVersion: RubyLsp::VERSION,
118
- requestTime: request_time,
119
- }
120
-
121
- if error
122
- params[:errorClass] = error.class.name
123
- params[:errorMessage] = error.message
124
- end
125
-
126
- params[:uri] = uri.sub(%r{.*://#{Dir.home}}, "~") if uri
127
- params
128
- end
129
117
  end
130
118
  end
@@ -0,0 +1,176 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "benchmark"
5
+
6
+ module RubyLsp
7
+ class Queue
8
+ extend T::Sig
9
+
10
+ class Result < T::Struct
11
+ const :response, T.untyped # rubocop:disable Sorbet/ForbidUntypedStructProps
12
+ const :error, T.nilable(Exception)
13
+ const :request_time, T.nilable(Float)
14
+ end
15
+
16
+ class Job < T::Struct
17
+ extend T::Sig
18
+
19
+ const :request, T::Hash[Symbol, T.untyped]
20
+ prop :cancelled, T::Boolean
21
+
22
+ sig { void }
23
+ def cancel
24
+ self.cancelled = true
25
+ end
26
+ end
27
+
28
+ sig do
29
+ params(
30
+ writer: LanguageServer::Protocol::Transport::Stdio::Writer,
31
+ handlers: T::Hash[String, Handler::RequestHandler]
32
+ ).void
33
+ end
34
+ def initialize(writer, handlers)
35
+ @writer = writer
36
+ @handlers = handlers
37
+ # The job queue is the actual list of requests we have to process
38
+ @job_queue = T.let(Thread::Queue.new, Thread::Queue)
39
+ # The jobs hash is just a way of keeping a handle to jobs based on the request ID, so we can cancel them
40
+ @jobs = T.let({}, T::Hash[T.any(String, Integer), Job])
41
+ @mutex = T.let(Mutex.new, Mutex)
42
+ @worker = T.let(new_worker, Thread)
43
+
44
+ Thread.main.priority = 1
45
+ end
46
+
47
+ sig { params(request: T::Hash[Symbol, T.untyped]).void }
48
+ def push(request)
49
+ job = Job.new(request: request, cancelled: false)
50
+
51
+ # Remember a handle to the job, so that we can cancel it
52
+ @mutex.synchronize do
53
+ @jobs[request[:id]] = job
54
+ end
55
+
56
+ @job_queue << job
57
+ end
58
+
59
+ sig { params(id: T.any(String, Integer)).void }
60
+ def cancel(id)
61
+ @mutex.synchronize do
62
+ # Cancel the job if it's still in the queue
63
+ @jobs[id]&.cancel
64
+ end
65
+ end
66
+
67
+ sig { void }
68
+ def shutdown
69
+ # Close the queue so that we can no longer receive items
70
+ @job_queue.close
71
+ # Clear any remaining jobs so that the thread can terminate
72
+ @job_queue.clear
73
+ # Wait until the thread is finished
74
+ @worker.join
75
+ end
76
+
77
+ # Executes a request and returns a Queue::Result. No IO should happen in this method, because it can be cancelled in
78
+ # the middle with a raise
79
+ sig { params(request: T::Hash[Symbol, T.untyped]).returns(Queue::Result) }
80
+ def execute(request)
81
+ response = T.let(nil, T.untyped)
82
+ error = T.let(nil, T.nilable(Exception))
83
+
84
+ request_time = Benchmark.realtime do
85
+ response = T.must(@handlers[request[:method]]).action.call(request)
86
+ rescue StandardError, LoadError => e
87
+ error = e
88
+ end
89
+
90
+ Queue::Result.new(response: response, error: error, request_time: request_time)
91
+ end
92
+
93
+ # Finalize a Queue::Result. All IO operations should happen here to avoid any issues with cancelling requests
94
+ sig do
95
+ params(
96
+ result: Result,
97
+ request: T::Hash[Symbol, T.untyped]
98
+ ).void
99
+ end
100
+ def finalize_request(result, request)
101
+ @mutex.synchronize do
102
+ error = result.error
103
+ if error
104
+ T.must(@handlers[request[:method]]).error_handler&.call(error, request)
105
+
106
+ @writer.write(
107
+ id: request[:id],
108
+ error: {
109
+ code: LanguageServer::Protocol::Constant::ErrorCodes::INTERNAL_ERROR,
110
+ message: result.error.inspect,
111
+ data: request.to_json,
112
+ },
113
+ )
114
+ elsif result.response != Handler::VOID
115
+ @writer.write(id: request[:id], result: result.response)
116
+ end
117
+
118
+ request_time = result.request_time
119
+ if request_time
120
+ @writer.write(method: "telemetry/event", params: telemetry_params(request, request_time, result.error))
121
+ end
122
+ end
123
+ end
124
+
125
+ private
126
+
127
+ sig { returns(Thread) }
128
+ def new_worker
129
+ Thread.new do
130
+ # Thread::Queue#pop is thread safe and will wait until an item is available
131
+ loop do
132
+ job = T.let(@job_queue.pop, T.nilable(Job))
133
+ # The only time when the job is nil is when the queue is closed and we can then terminate the thread
134
+ break if job.nil?
135
+
136
+ request = job.request
137
+ @mutex.synchronize { @jobs.delete(request[:id]) }
138
+
139
+ result = if job.cancelled
140
+ # We need to return nil to the client even if the request was cancelled
141
+ Queue::Result.new(response: nil, error: nil, request_time: nil)
142
+ else
143
+ execute(request)
144
+ end
145
+
146
+ finalize_request(result, request)
147
+ end
148
+ end
149
+ end
150
+
151
+ sig do
152
+ params(
153
+ request: T::Hash[Symbol, T.untyped],
154
+ request_time: Float,
155
+ error: T.nilable(Exception)
156
+ ).returns(T::Hash[Symbol, T.any(String, Float)])
157
+ end
158
+ def telemetry_params(request, request_time, error)
159
+ uri = request.dig(:params, :textDocument, :uri)
160
+
161
+ params = {
162
+ request: request[:method],
163
+ lspVersion: RubyLsp::VERSION,
164
+ requestTime: request_time,
165
+ }
166
+
167
+ if error
168
+ params[:errorClass] = error.class.name
169
+ params[:errorMessage] = error.message
170
+ end
171
+
172
+ params[:uri] = uri.sub(%r{.*://#{Dir.home}}, "~") if uri
173
+ params
174
+ end
175
+ end
176
+ end
@@ -105,9 +105,13 @@ module RubyLsp
105
105
  sig { returns(Integer) }
106
106
  attr_reader :end_line
107
107
 
108
- sig { params(node: SyntaxTree::Node, kind: String).returns(PartialRange) }
109
- def self.from(node, kind)
110
- new(node.location.start_line - 1, node.location.end_line - 1, kind)
108
+ class << self
109
+ extend T::Sig
110
+
111
+ sig { params(node: SyntaxTree::Node, kind: String).returns(PartialRange) }
112
+ def from(node, kind)
113
+ new(node.location.start_line - 1, node.location.end_line - 1, kind)
114
+ end
111
115
  end
112
116
 
113
117
  sig { params(start_line: Integer, end_line: Integer, kind: String).void }
@@ -19,6 +19,8 @@ module RubyLsp
19
19
  # end
20
20
  # ```
21
21
  class Formatting < BaseRequest
22
+ class Error < StandardError; end
23
+
22
24
  extend T::Sig
23
25
 
24
26
  sig { params(uri: String, document: Document).void }
@@ -0,0 +1,149 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Requests
6
+ # ![On type formatting demo](../../misc/on_type_formatting.gif)
7
+ #
8
+ # The [on type formatting](https://microsoft.github.io/language-server-protocol/specification#textDocument_onTypeFormatting)
9
+ # request formats code as the user is typing. For example, automatically adding `end` to class definitions.
10
+ #
11
+ # # Example
12
+ #
13
+ # ```ruby
14
+ # class Foo # <-- upon adding a line break, on type formatting is triggered
15
+ # # <-- cursor ends up here
16
+ # end # <-- end is automatically added
17
+ # ```
18
+ class OnTypeFormatting < BaseRequest
19
+ extend T::Sig
20
+
21
+ END_REGEXES = T.let([
22
+ /(if|unless|for|while|class|module|until|def|case).*/,
23
+ /.*\sdo/,
24
+ ], T::Array[Regexp])
25
+
26
+ sig { params(document: Document, position: Document::PositionShape, trigger_character: String).void }
27
+ def initialize(document, position, trigger_character)
28
+ super(document)
29
+
30
+ scanner = Document::Scanner.new(document.source)
31
+ line_begin = position[:line] == 0 ? 0 : scanner.find_position({ line: position[:line] - 1, character: 0 })
32
+ line_end = scanner.find_position(position)
33
+ line = T.must(@document.source[line_begin..line_end])
34
+
35
+ @indentation = T.let(find_indentation(line), Integer)
36
+ @previous_line = T.let(line.strip.chomp, String)
37
+ @position = position
38
+ @edits = T.let([], T::Array[Interface::TextEdit])
39
+ @trigger_character = trigger_character
40
+ end
41
+
42
+ sig { override.returns(T.nilable(T.all(T::Array[Interface::TextEdit], Object))) }
43
+ def run
44
+ handle_comment_line
45
+
46
+ return @edits unless @document.syntax_errors?
47
+
48
+ case @trigger_character
49
+ when "{"
50
+ handle_curly_brace
51
+ when "|"
52
+ handle_pipe
53
+ when "\n"
54
+ handle_statement_end
55
+ end
56
+
57
+ @edits
58
+ end
59
+
60
+ private
61
+
62
+ sig { void }
63
+ def handle_pipe
64
+ return unless /((?<=do)|(?<={))\s+\|/.match?(@previous_line)
65
+
66
+ add_edit_with_text("|")
67
+ move_cursor_to(@position[:line], @position[:character])
68
+ end
69
+
70
+ sig { void }
71
+ def handle_curly_brace
72
+ return unless /".*#\{/.match?(@previous_line)
73
+
74
+ add_edit_with_text("}")
75
+ move_cursor_to(@position[:line], @position[:character])
76
+ end
77
+
78
+ sig { void }
79
+ def handle_statement_end
80
+ return unless END_REGEXES.any? { |regex| regex.match?(@previous_line) }
81
+
82
+ indents = " " * @indentation
83
+
84
+ add_edit_with_text(" \n#{indents}end")
85
+ move_cursor_to(@position[:line], @indentation + 2)
86
+ end
87
+
88
+ sig { void }
89
+ def handle_comment_line
90
+ return unless @trigger_character == "\n"
91
+
92
+ is_comment_match = @previous_line.match(/^#(\s*)/)
93
+ return unless is_comment_match
94
+
95
+ spaces = T.must(is_comment_match[1])
96
+ add_edit_with_text("##{spaces}")
97
+ move_cursor_to(@position[:line], @indentation + spaces.size + 1)
98
+ end
99
+
100
+ sig { params(text: String).void }
101
+ def add_edit_with_text(text)
102
+ position = Interface::Position.new(
103
+ line: @position[:line],
104
+ character: @position[:character]
105
+ )
106
+
107
+ @edits << Interface::TextEdit.new(
108
+ range: Interface::Range.new(
109
+ start: position,
110
+ end: position
111
+ ),
112
+ new_text: text
113
+ )
114
+ end
115
+
116
+ sig { params(line: Integer, character: Integer).void }
117
+ def move_cursor_to(line, character)
118
+ position = Interface::Position.new(
119
+ line: line,
120
+ character: character
121
+ )
122
+
123
+ # The $0 is a special snippet anchor that moves the cursor to that given position. See the snippets
124
+ # documentation for more information:
125
+ # https://code.visualstudio.com/docs/editor/userdefinedsnippets#_create-your-own-snippets
126
+ @edits << Interface::TextEdit.new(
127
+ range: Interface::Range.new(
128
+ start: position,
129
+ end: position
130
+ ),
131
+ new_text: "$0"
132
+ )
133
+ end
134
+
135
+ sig { params(line: String).returns(Integer) }
136
+ def find_indentation(line)
137
+ count = 0
138
+
139
+ line.chars.each do |c|
140
+ break unless c == " "
141
+
142
+ count += 1
143
+ end
144
+
145
+ count
146
+ end
147
+ end
148
+ end
149
+ end
@@ -185,6 +185,10 @@ module RubyLsp
185
185
  add_token(location_without_colon(location), :variable)
186
186
  end
187
187
 
188
+ node.requireds.each do |required|
189
+ add_token(required.location, :variable)
190
+ end
191
+
188
192
  rest = node.keyword_rest
189
193
  return if rest.nil? || rest.is_a?(SyntaxTree::ArgsForward)
190
194
 
@@ -217,6 +221,19 @@ module RubyLsp
217
221
  add_token(node.value.location, :method) unless special_method?(node.value.value)
218
222
  end
219
223
 
224
+ sig { params(node: SyntaxTree::ClassDeclaration).void }
225
+ def visit_class(node)
226
+ add_token(node.constant.location, :class, [:declaration])
227
+ add_token(node.superclass.location, :class) if node.superclass
228
+ visit(node.bodystmt)
229
+ end
230
+
231
+ sig { params(node: SyntaxTree::ModuleDeclaration).void }
232
+ def visit_module(node)
233
+ add_token(node.constant.location, :class, [:declaration])
234
+ visit(node.bodystmt)
235
+ end
236
+
220
237
  sig { params(location: SyntaxTree::Location, type: Symbol, modifiers: T::Array[Symbol]).void }
221
238
  def add_token(location, type, modifiers = [])
222
239
  length = location.end_char - location.start_char
@@ -1,11 +1,8 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- begin
5
- require "rubocop"
6
- rescue LoadError
7
- return
8
- end
4
+ require "ruby_lsp/requests/support/rubocop_runner"
5
+ return unless defined?(::RubyLsp::Requests::Support::RuboCopRunner)
9
6
 
10
7
  require "cgi"
11
8
  require "singleton"
@@ -14,46 +11,24 @@ module RubyLsp
14
11
  module Requests
15
12
  module Support
16
13
  # :nodoc:
17
- class RuboCopDiagnosticsRunner < RuboCop::Runner
14
+ class RuboCopDiagnosticsRunner
18
15
  extend T::Sig
19
16
  include Singleton
20
17
 
21
18
  sig { void }
22
19
  def initialize
23
- @options = T.let({}, T::Hash[Symbol, T.untyped])
24
- @uri = T.let(nil, T.nilable(String))
25
- @diagnostics = T.let([], T::Array[Support::RuboCopDiagnostic])
26
-
27
- super(
28
- ::RuboCop::Options.new.parse([
29
- "--stderr", # Print any output to stderr so that our stdout does not get polluted
30
- "--force-exclusion",
31
- "--format",
32
- "RuboCop::Formatter::BaseFormatter", # Suppress any output by using the base formatter
33
- ]).first,
34
- ::RuboCop::ConfigStore.new
35
- )
20
+ @runner = T.let(RuboCopRunner.new, RuboCopRunner)
36
21
  end
37
22
 
38
23
  sig { params(uri: String, document: Document).returns(T::Array[Support::RuboCopDiagnostic]) }
39
24
  def run(uri, document)
40
- @diagnostics.clear
41
- @uri = uri
42
-
43
- file = CGI.unescape(URI.parse(uri).path)
44
- # We communicate with Rubocop via stdin
45
- @options[:stdin] = document.source
46
-
25
+ filename = CGI.unescape(URI.parse(uri).path)
47
26
  # Invoke RuboCop with just this file in `paths`
48
- super([file])
49
- @diagnostics
50
- end
51
-
52
- private
27
+ @runner.run(filename, document.source)
53
28
 
54
- sig { params(_file: String, offenses: T::Array[RuboCop::Cop::Offense]).void }
55
- def file_finished(_file, offenses)
56
- @diagnostics = offenses.map { |offense| Support::RuboCopDiagnostic.new(offense, T.must(@uri)) }
29
+ @runner.offenses.map do |offense|
30
+ Support::RuboCopDiagnostic.new(offense, uri)
31
+ end
57
32
  end
58
33
  end
59
34
  end
@@ -1,11 +1,8 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- begin
5
- require "rubocop"
6
- rescue LoadError
7
- return
8
- end
4
+ require "ruby_lsp/requests/support/rubocop_runner"
5
+ return unless defined?(::RubyLsp::Requests::Support::RuboCopRunner)
9
6
 
10
7
  require "cgi"
11
8
  require "singleton"
@@ -14,35 +11,24 @@ module RubyLsp
14
11
  module Requests
15
12
  module Support
16
13
  # :nodoc:
17
- class RuboCopFormattingRunner < RuboCop::Runner
14
+ class RuboCopFormattingRunner
18
15
  extend T::Sig
19
16
  include Singleton
20
17
 
21
18
  sig { void }
22
19
  def initialize
23
- @options = T.let({}, T::Hash[Symbol, T.untyped])
24
-
25
- super(
26
- ::RuboCop::Options.new.parse([
27
- "--stderr", # Print any output to stderr so that our stdout does not get polluted
28
- "--force-exclusion",
29
- "--format",
30
- "RuboCop::Formatter::BaseFormatter", # Suppress any output by using the base formatter
31
- "-a", # --auto-correct
32
- ]).first,
33
- ::RuboCop::ConfigStore.new
34
- )
20
+ # -a is for "--auto-correct" (or "--autocorrect" on newer versions of RuboCop)
21
+ @runner = T.let(RuboCopRunner.new("-a"), RuboCopRunner)
35
22
  end
36
23
 
37
24
  sig { params(uri: String, document: Document).returns(T.nilable(String)) }
38
25
  def run(uri, document)
39
- file = CGI.unescape(URI.parse(uri).path)
40
- # We communicate with Rubocop via stdin
41
- @options[:stdin] = document.source
26
+ filename = CGI.unescape(URI.parse(uri).path)
42
27
 
43
28
  # Invoke RuboCop with just this file in `paths`
44
- super([file])
45
- @options[:stdin]
29
+ @runner.run(filename, document.source)
30
+
31
+ @runner.formatted_source
46
32
  end
47
33
  end
48
34
  end
@@ -0,0 +1,73 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require "rubocop"
6
+ rescue LoadError
7
+ return
8
+ end
9
+
10
+ module RubyLsp
11
+ module Requests
12
+ module Support
13
+ # :nodoc:
14
+ class RuboCopRunner < RuboCop::Runner
15
+ extend T::Sig
16
+
17
+ class ConfigurationError < StandardError; end
18
+
19
+ sig { returns(T::Array[RuboCop::Cop::Offense]) }
20
+ attr_reader :offenses
21
+
22
+ DEFAULT_ARGS = T.let([
23
+ "--stderr", # Print any output to stderr so that our stdout does not get polluted
24
+ "--force-exclusion",
25
+ "--format",
26
+ "RuboCop::Formatter::BaseFormatter", # Suppress any output by using the base formatter
27
+ ].freeze, T::Array[String])
28
+
29
+ sig { params(args: String).void }
30
+ def initialize(*args)
31
+ @options = T.let({}, T::Hash[Symbol, T.untyped])
32
+ @offenses = T.let([], T::Array[RuboCop::Cop::Offense])
33
+ @errors = T.let([], T::Array[String])
34
+ @warnings = T.let([], T::Array[String])
35
+
36
+ args += DEFAULT_ARGS
37
+ rubocop_options = ::RuboCop::Options.new.parse(args).first
38
+ config_store = ::RuboCop::ConfigStore.new
39
+
40
+ super(rubocop_options, config_store)
41
+ end
42
+
43
+ sig { params(path: String, contents: String).void }
44
+ def run(path, contents)
45
+ # Clear Runner state between runs since we get a single instance of this class
46
+ # on every use site.
47
+ @errors = []
48
+ @warnings = []
49
+ @offenses = []
50
+ @options[:stdin] = contents
51
+
52
+ super([path])
53
+ rescue RuboCop::Runner::InfiniteCorrectionLoop => error
54
+ raise Formatting::Error, error.message
55
+ rescue RuboCop::ValidationError => error
56
+ raise ConfigurationError, error.message
57
+ end
58
+
59
+ sig { returns(String) }
60
+ def formatted_source
61
+ @options[:stdin]
62
+ end
63
+
64
+ private
65
+
66
+ sig { params(_file: String, offenses: T::Array[RuboCop::Cop::Offense]).void }
67
+ def file_finished(_file, offenses)
68
+ @offenses = offenses
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -10,6 +10,7 @@ module RubyLsp
10
10
  # - {RubyLsp::Requests::SelectionRanges}
11
11
  # - {RubyLsp::Requests::SemanticHighlighting}
12
12
  # - {RubyLsp::Requests::Formatting}
13
+ # - {RubyLsp::Requests::OnTypeFormatting}
13
14
  # - {RubyLsp::Requests::Diagnostics}
14
15
  # - {RubyLsp::Requests::CodeActions}
15
16
  # - {RubyLsp::Requests::DocumentHighlight}
@@ -21,6 +22,7 @@ module RubyLsp
21
22
  autoload :SelectionRanges, "ruby_lsp/requests/selection_ranges"
22
23
  autoload :SemanticHighlighting, "ruby_lsp/requests/semantic_highlighting"
23
24
  autoload :Formatting, "ruby_lsp/requests/formatting"
25
+ autoload :OnTypeFormatting, "ruby_lsp/requests/on_type_formatting"
24
26
  autoload :Diagnostics, "ruby_lsp/requests/diagnostics"
25
27
  autoload :CodeActions, "ruby_lsp/requests/code_actions"
26
28
  autoload :DocumentHighlight, "ruby_lsp/requests/document_highlight"
@@ -48,23 +48,30 @@ module RubyLsp
48
48
  }
49
49
  end
50
50
 
51
- # TODO: switch back to using Interface::ServerCapabilities once the gem is updated for spec 3.17
51
+ on_type_formatting_provider = if enabled_features.include?("onTypeFormatting")
52
+ Interface::DocumentOnTypeFormattingOptions.new(
53
+ first_trigger_character: "{",
54
+ more_trigger_character: ["\n", "|"]
55
+ )
56
+ end
57
+
52
58
  Interface::InitializeResult.new(
53
- capabilities: {
54
- textDocumentSync: Interface::TextDocumentSyncOptions.new(
59
+ capabilities: Interface::ServerCapabilities.new(
60
+ text_document_sync: Interface::TextDocumentSyncOptions.new(
55
61
  change: Constant::TextDocumentSyncKind::INCREMENTAL,
56
62
  open_close: true,
57
63
  ),
58
- selectionRangeProvider: enabled_features.include?("selectionRanges"),
59
- documentSymbolProvider: document_symbol_provider,
60
- documentLinkProvider: document_link_provider,
61
- foldingRangeProvider: folding_ranges_provider,
62
- semanticTokensProvider: semantic_tokens_provider,
63
- documentFormattingProvider: enabled_features.include?("formatting"),
64
- documentHighlightProvider: enabled_features.include?("documentHighlights"),
65
- codeActionProvider: enabled_features.include?("codeActions"),
66
- diagnosticProvider: diagnostics_provider,
67
- }.reject { |_, v| !v }
64
+ selection_range_provider: enabled_features.include?("selectionRanges"),
65
+ document_symbol_provider: document_symbol_provider,
66
+ document_link_provider: document_link_provider,
67
+ folding_range_provider: folding_ranges_provider,
68
+ semantic_tokens_provider: semantic_tokens_provider,
69
+ document_formatting_provider: enabled_features.include?("formatting"),
70
+ document_highlight_provider: enabled_features.include?("documentHighlights"),
71
+ code_action_provider: enabled_features.include?("codeActions"),
72
+ document_on_type_formatting_provider: on_type_formatting_provider,
73
+ diagnostic_provider: diagnostics_provider,
74
+ )
68
75
  )
69
76
  end
70
77
 
@@ -91,26 +98,26 @@ module RubyLsp
91
98
  Handler::VOID
92
99
  end
93
100
 
94
- on("textDocument/documentSymbol") do |request|
101
+ on("textDocument/documentSymbol", parallel: true) do |request|
95
102
  store.cache_fetch(request.dig(:params, :textDocument, :uri), :document_symbol) do |document|
96
103
  Requests::DocumentSymbol.new(document).run
97
104
  end
98
105
  end
99
106
 
100
- on("textDocument/documentLink") do |request|
107
+ on("textDocument/documentLink", parallel: true) do |request|
101
108
  uri = request.dig(:params, :textDocument, :uri)
102
109
  store.cache_fetch(uri, :document_link) do |document|
103
110
  RubyLsp::Requests::DocumentLink.new(uri, document).run
104
111
  end
105
112
  end
106
113
 
107
- on("textDocument/foldingRange") do |request|
114
+ on("textDocument/foldingRange", parallel: true) do |request|
108
115
  store.cache_fetch(request.dig(:params, :textDocument, :uri), :folding_ranges) do |document|
109
116
  Requests::FoldingRanges.new(document).run
110
117
  end
111
118
  end
112
119
 
113
- on("textDocument/selectionRange") do |request|
120
+ on("textDocument/selectionRange", parallel: true) do |request|
114
121
  uri = request.dig(:params, :textDocument, :uri)
115
122
  positions = request.dig(:params, :positions)
116
123
 
@@ -132,7 +139,7 @@ module RubyLsp
132
139
  end
133
140
  end
134
141
 
135
- on("textDocument/semanticTokens/full") do |request|
142
+ on("textDocument/semanticTokens/full", parallel: true) do |request|
136
143
  store.cache_fetch(request.dig(:params, :textDocument, :uri), :semantic_highlighting) do |document|
137
144
  T.cast(
138
145
  Requests::SemanticHighlighting.new(
@@ -144,13 +151,23 @@ module RubyLsp
144
151
  end
145
152
  end
146
153
 
147
- on("textDocument/formatting") do |request|
154
+ on("textDocument/formatting", parallel: true) do |request|
148
155
  uri = request.dig(:params, :textDocument, :uri)
149
156
 
150
157
  Requests::Formatting.new(uri, store.get(uri)).run
158
+ end.on_error do |error|
159
+ show_message(Constant::MessageType::ERROR, "Formatting error: #{error.message}")
151
160
  end
152
161
 
153
- on("textDocument/documentHighlight") do |request|
162
+ on("textDocument/onTypeFormatting", parallel: true) do |request|
163
+ uri = request.dig(:params, :textDocument, :uri)
164
+ position = request.dig(:params, :position)
165
+ character = request.dig(:params, :ch)
166
+
167
+ Requests::OnTypeFormatting.new(store.get(uri), position, character).run
168
+ end
169
+
170
+ on("textDocument/documentHighlight", parallel: true) do |request|
154
171
  document = store.get(request.dig(:params, :textDocument, :uri))
155
172
 
156
173
  if document.parsed?
@@ -158,7 +175,7 @@ module RubyLsp
158
175
  end
159
176
  end
160
177
 
161
- on("textDocument/codeAction") do |request|
178
+ on("textDocument/codeAction", parallel: true) do |request|
162
179
  uri = request.dig(:params, :textDocument, :uri)
163
180
  range = request.dig(:params, :range)
164
181
  start_line = range.dig(:start, :line)
@@ -169,16 +186,20 @@ module RubyLsp
169
186
  end
170
187
  end
171
188
 
172
- on("textDocument/diagnostic") do |request|
189
+ on("$/cancelRequest") do |request|
190
+ cancel_request(request[:params][:id])
191
+ Handler::VOID
192
+ end
193
+
194
+ on("textDocument/diagnostic", parallel: true) do |request|
173
195
  uri = request.dig(:params, :textDocument, :uri)
174
196
  response = store.cache_fetch(uri, :diagnostics) do |document|
175
197
  Requests::Diagnostics.new(uri, document).run
176
198
  end
177
199
 
178
200
  { kind: "full", items: response.map(&:to_lsp_diagnostic) } if response
179
- rescue RuboCop::ValidationError => e
180
- show_message(Constant::MessageType::ERROR, "Error in RuboCop configuration file: #{e.message}")
181
- nil
201
+ end.on_error do |error|
202
+ show_message(Constant::MessageType::ERROR, "Error running diagnostics: #{error.message}")
182
203
  end
183
204
 
184
205
  on("shutdown") { shutdown }
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-lsp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-08-26 00:00:00.000000000 Z
11
+ date: 2022-09-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: language_server-protocol
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: 3.17.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: 3.17.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: sorbet-runtime
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '2.4'
47
+ version: '3.4'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: '2.4'
54
+ version: '3.4'
55
55
  description: An opinionated language server for Ruby
56
56
  email:
57
57
  - ruby@shopify.com
@@ -69,6 +69,7 @@ files:
69
69
  - lib/ruby_lsp/document.rb
70
70
  - lib/ruby_lsp/handler.rb
71
71
  - lib/ruby_lsp/internal.rb
72
+ - lib/ruby_lsp/queue.rb
72
73
  - lib/ruby_lsp/requests.rb
73
74
  - lib/ruby_lsp/requests/base_request.rb
74
75
  - lib/ruby_lsp/requests/code_actions.rb
@@ -78,12 +79,14 @@ files:
78
79
  - lib/ruby_lsp/requests/document_symbol.rb
79
80
  - lib/ruby_lsp/requests/folding_ranges.rb
80
81
  - lib/ruby_lsp/requests/formatting.rb
82
+ - lib/ruby_lsp/requests/on_type_formatting.rb
81
83
  - lib/ruby_lsp/requests/selection_ranges.rb
82
84
  - lib/ruby_lsp/requests/semantic_highlighting.rb
83
85
  - lib/ruby_lsp/requests/support/highlight_target.rb
84
86
  - lib/ruby_lsp/requests/support/rubocop_diagnostic.rb
85
87
  - lib/ruby_lsp/requests/support/rubocop_diagnostics_runner.rb
86
88
  - lib/ruby_lsp/requests/support/rubocop_formatting_runner.rb
89
+ - lib/ruby_lsp/requests/support/rubocop_runner.rb
87
90
  - lib/ruby_lsp/requests/support/selection_range.rb
88
91
  - lib/ruby_lsp/requests/support/semantic_token_encoder.rb
89
92
  - lib/ruby_lsp/requests/support/source_uri.rb