ruby-lsp 0.2.3 → 0.3.1

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: 96c21e90970f4cbf198609a42471cf554ce86a3b6af9296ded95994ad373317a
4
- data.tar.gz: a28aa2c13ba4d79f53082160b0eca7e8461efad831b6c6cca2a6a4d34765a6c0
3
+ metadata.gz: 9762389a7b7979afb57d6e6d2cf0d65b0b0a0b31f5a1fdd7045311c33a0c21d6
4
+ data.tar.gz: 35cc2318946e650ee311129955d3674c7e693a87d25c5cc2f29c5a55e479d880
5
5
  SHA512:
6
- metadata.gz: 5ab1bf48ae4943a710f34ef0aace64c1d8991271bdca71482a2367c3a103299c52ebaef203fa32945236d689dda67d2bb1b5cc5ca9ffa698f5dd057dd62e242f
7
- data.tar.gz: 8b9453d71b65634e1ea275f3a2552548f530dc144acd18cdcbcee493f153b824a13bc60bbbe5ac937a993d9e9c56ce8cb70773d354b5cea7acda3f4f44722ffd
6
+ metadata.gz: 4735e18c297c992727d0a151ed7948cf6d4ea32b01fe411ed0774efdda7cdc0cf6533f5d71ad288aad9e6e717149932744376fdceda9fa4cd039b560934a9b47
7
+ data.tar.gz: bd6e0c9a81a373c509f2f96b480543657a8d241a6e58ea25ec21c62ac60094f299af1c5f85d1555ad6ca1908c325e46569119271d4c6b7117f33f9919cf40bca
data/CHANGELOG.md CHANGED
@@ -6,32 +6,24 @@ 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.1]
10
+
11
+ - Resolve TODO for LSP v3.17 (https://github.com/Shopify/ruby-lsp/pull/268)
12
+ - Add dependency constraint for LSP v3.17 (https://github.com/Shopify/ruby-lsp/pull/269)
13
+ - Handle class/module declarations as a class token with declaration modifier (https://github.com/Shopify/ruby-lsp/pull/260)
14
+ - Handle required parameters in semantic highlighting (https://github.com/Shopify/ruby-lsp/pull/271)
15
+ - Add comment continuation via on type on_type_formatting (https://github.com/Shopify/ruby-lsp/pull/274)
16
+ - Make RuboCop runner use composition instead of inheritance (https://github.com/Shopify/ruby-lsp/pull/278)
17
+ - Protect worker against cancellation during popping (https://github.com/Shopify/ruby-lsp/pull/280)
18
+ - Handle formatting errors in on_error block (https://github.com/Shopify/ruby-lsp/pull/279)
19
+ - Fix on type formatting pipe completion for regular or expressions (https://github.com/Shopify/ruby-lsp/pull/282)
20
+ - Do not fail on LoadError (https://github.com/Shopify/ruby-lsp/pull/292)
21
+
22
+ ## [0.3.0]
23
+ - Add on type formatting completions (https://github.com/Shopify/ruby-lsp/pull/253)
24
+ - Upgrade syntax_tree requirement to >= 3.4 (https://github.com/Shopify/ruby-lsp/pull/254)
25
+ - Show error message when there's a InfiniteCorrectionLoop exception (https://github.com/Shopify/ruby-lsp/pull/252)
26
+ - Add request cancellation (https://github.com/Shopify/ruby-lsp/pull/243)
35
27
 
36
28
  ## [0.2.0]
37
29
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.3
1
+ 0.3.1
@@ -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,193 @@
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 Cancelled < StandardError; end
11
+
12
+ class Result < T::Struct
13
+ const :response, T.untyped # rubocop:disable Sorbet/ForbidUntypedStructProps
14
+ const :error, T.nilable(Exception)
15
+ const :request_time, T.nilable(Float)
16
+ end
17
+
18
+ class Job < T::Struct
19
+ extend T::Sig
20
+
21
+ const :request, T::Hash[Symbol, T.untyped]
22
+ prop :cancelled, T::Boolean
23
+
24
+ sig { void }
25
+ def cancel
26
+ self.cancelled = true
27
+ end
28
+ end
29
+
30
+ sig do
31
+ params(
32
+ writer: LanguageServer::Protocol::Transport::Stdio::Writer,
33
+ handlers: T::Hash[String, Handler::RequestHandler]
34
+ ).void
35
+ end
36
+ def initialize(writer, handlers)
37
+ @writer = writer
38
+ @handlers = handlers
39
+ # The job queue is the actual list of requests we have to process
40
+ @job_queue = T.let(Thread::Queue.new, Thread::Queue)
41
+ # The jobs hash is just a way of keeping a handle to jobs based on the request ID, so we can cancel them
42
+ @jobs = T.let({}, T::Hash[T.any(String, Integer), Job])
43
+ # The current job is a handle to cancel jobs that are currently being processed
44
+ @current_job = T.let(nil, T.nilable(Job))
45
+ @mutex = T.let(Mutex.new, Mutex)
46
+ @worker = T.let(new_worker, Thread)
47
+
48
+ Thread.main.priority = 1
49
+ end
50
+
51
+ sig { params(request: T::Hash[Symbol, T.untyped]).void }
52
+ def push(request)
53
+ job = Job.new(request: request, cancelled: false)
54
+
55
+ # Remember a handle to the job, so that we can cancel it
56
+ @mutex.synchronize do
57
+ @jobs[request[:id]] = job
58
+ end
59
+
60
+ @job_queue << job
61
+ end
62
+
63
+ sig { params(id: T.any(String, Integer)).void }
64
+ def cancel(id)
65
+ @mutex.synchronize do
66
+ # Cancel the job if it's still in the queue
67
+ @jobs[id]&.cancel
68
+
69
+ # Cancel the job if we're in the middle of processing it
70
+ if @current_job&.request&.dig(:id) == id
71
+ @worker.raise(Cancelled)
72
+ end
73
+ end
74
+ end
75
+
76
+ sig { void }
77
+ def shutdown
78
+ # Close the queue so that we can no longer receive items
79
+ @job_queue.close
80
+ # Clear any remaining jobs so that the thread can terminate
81
+ @job_queue.clear
82
+ # Wait until the thread is finished
83
+ @worker.join
84
+ end
85
+
86
+ # Executes a request and returns a Queue::Result. No IO should happen in this method, because it can be cancelled in
87
+ # the middle with a raise
88
+ sig { params(request: T::Hash[Symbol, T.untyped]).returns(Queue::Result) }
89
+ def execute(request)
90
+ response = T.let(nil, T.untyped)
91
+ error = T.let(nil, T.nilable(Exception))
92
+
93
+ request_time = Benchmark.realtime do
94
+ response = T.must(@handlers[request[:method]]).action.call(request)
95
+ rescue Cancelled
96
+ raise
97
+ rescue StandardError, LoadError => e
98
+ error = e
99
+ end
100
+
101
+ Queue::Result.new(response: response, error: error, request_time: request_time)
102
+ end
103
+
104
+ # Finalize a Queue::Result. All IO operations should happen here to avoid any issues with cancelling requests
105
+ sig do
106
+ params(
107
+ result: Result,
108
+ request: T::Hash[Symbol, T.untyped]
109
+ ).void
110
+ end
111
+ def finalize_request(result, request)
112
+ error = result.error
113
+ if error
114
+ T.must(@handlers[request[:method]]).error_handler&.call(error, request)
115
+
116
+ @writer.write(
117
+ id: request[:id],
118
+ error: {
119
+ code: LanguageServer::Protocol::Constant::ErrorCodes::INTERNAL_ERROR,
120
+ message: result.error.inspect,
121
+ data: request.to_json,
122
+ },
123
+ )
124
+ elsif result.response != Handler::VOID
125
+ @writer.write(id: request[:id], result: result.response)
126
+ end
127
+
128
+ request_time = result.request_time
129
+ if request_time
130
+ @writer.write(method: "telemetry/event", params: telemetry_params(request, request_time, result.error))
131
+ end
132
+ end
133
+
134
+ private
135
+
136
+ sig { returns(Thread) }
137
+ def new_worker
138
+ Thread.new do
139
+ # Thread::Queue#pop is thread safe and will wait until an item is available
140
+ loop do
141
+ job = T.let(@job_queue.pop, T.nilable(Job))
142
+ break if job.nil?
143
+
144
+ # The only time when the job is nil is when the queue is closed and we can then terminate the thread
145
+
146
+ request = job.request
147
+ @mutex.synchronize do
148
+ @jobs.delete(request[:id])
149
+ @current_job = job
150
+ end
151
+
152
+ next if job.cancelled
153
+
154
+ result = execute(request)
155
+ rescue Cancelled
156
+ # We need to return nil to the client even if the request was cancelled
157
+ result = Queue::Result.new(response: nil, error: nil, request_time: nil)
158
+ ensure
159
+ @mutex.synchronize { @current_job = nil }
160
+
161
+ # If there's request, it means the worker was cancelled while waiting to pop from the queue or immediately
162
+ # after
163
+ finalize_request(result, request) unless result.nil? || request.nil?
164
+ end
165
+ end
166
+ end
167
+
168
+ sig do
169
+ params(
170
+ request: T::Hash[Symbol, T.untyped],
171
+ request_time: Float,
172
+ error: T.nilable(Exception)
173
+ ).returns(T::Hash[Symbol, T.any(String, Float)])
174
+ end
175
+ def telemetry_params(request, request_time, error)
176
+ uri = request.dig(:params, :textDocument, :uri)
177
+
178
+ params = {
179
+ request: request[:method],
180
+ lspVersion: RubyLsp::VERSION,
181
+ requestTime: request_time,
182
+ }
183
+
184
+ if error
185
+ params[:errorClass] = error.class.name
186
+ params[:errorMessage] = error.message
187
+ end
188
+
189
+ params[:uri] = uri.sub(%r{.*://#{Dir.home}}, "~") if uri
190
+ params
191
+ end
192
+ end
193
+ 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,69 @@
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
+ sig { returns(T::Array[RuboCop::Cop::Offense]) }
18
+ attr_reader :offenses
19
+
20
+ DEFAULT_ARGS = T.let([
21
+ "--stderr", # Print any output to stderr so that our stdout does not get polluted
22
+ "--force-exclusion",
23
+ "--format",
24
+ "RuboCop::Formatter::BaseFormatter", # Suppress any output by using the base formatter
25
+ ].freeze, T::Array[String])
26
+
27
+ sig { params(args: String).void }
28
+ def initialize(*args)
29
+ @options = T.let({}, T::Hash[Symbol, T.untyped])
30
+ @offenses = T.let([], T::Array[RuboCop::Cop::Offense])
31
+ @errors = T.let([], T::Array[String])
32
+ @warnings = T.let([], T::Array[String])
33
+
34
+ args += DEFAULT_ARGS
35
+ rubocop_options = ::RuboCop::Options.new.parse(args).first
36
+ config_store = ::RuboCop::ConfigStore.new
37
+
38
+ super(rubocop_options, config_store)
39
+ end
40
+
41
+ sig { params(path: String, contents: String).void }
42
+ def run(path, contents)
43
+ # Clear Runner state between runs since we get a single instance of this class
44
+ # on every use site.
45
+ @errors = []
46
+ @warnings = []
47
+ @offenses = []
48
+ @options[:stdin] = contents
49
+
50
+ super([path])
51
+ rescue RuboCop::Runner::InfiniteCorrectionLoop => error
52
+ raise Formatting::Error, error.message
53
+ end
54
+
55
+ sig { returns(String) }
56
+ def formatted_source
57
+ @options[:stdin]
58
+ end
59
+
60
+ private
61
+
62
+ sig { params(_file: String, offenses: T::Array[RuboCop::Cop::Offense]).void }
63
+ def file_finished(_file, offenses)
64
+ @offenses = offenses
65
+ end
66
+ end
67
+ end
68
+ end
69
+ 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}")
160
+ end
161
+
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
151
168
  end
152
169
 
153
- on("textDocument/documentHighlight") do |request|
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,6 +186,11 @@ module RubyLsp
169
186
  end
170
187
  end
171
188
 
189
+ on("$/cancelRequest") do |request|
190
+ cancel_request(request[:params][:id])
191
+ Handler::VOID
192
+ end
193
+
172
194
  on("textDocument/diagnostic") do |request|
173
195
  uri = request.dig(:params, :textDocument, :uri)
174
196
  response = store.cache_fetch(uri, :diagnostics) do |document|
@@ -176,9 +198,10 @@ module RubyLsp
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
+ if error.is_a?(RuboCop::ValidationError)
203
+ show_message(Constant::MessageType::ERROR, "Error in RuboCop configuration file: #{error.message}")
204
+ end
182
205
  end
183
206
 
184
207
  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.3
4
+ version: 0.3.1
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-18 00:00:00.000000000 Z
11
+ date: 2022-09-06 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