ruby-lsp 0.2.4 → 0.3.2

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