ruby-lsp 0.2.2 → 0.3.0

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: 0fefb041d92fb2000d3da23248ed622abd596a95264b421349f27d104b85f997
4
- data.tar.gz: 312918f128e0a49a2f5b2066354df2fe45f9ed37af5e5eace6c76caa101155e5
3
+ metadata.gz: 6e66f508823e7d8e663c90e9990ae13bf26c3480d4ce9d7d43db14185518064d
4
+ data.tar.gz: 70cfc0be15d1ad988d66a641191fa64679109ae1f6be56afdc01ded665b0a439
5
5
  SHA512:
6
- metadata.gz: 7ab96bd356fe94e3a95572bec24053eed9b467c71acec431a80c600e87ea5a3c31f0f2aee20c7b3b407b5aabc9b5736e0bb385dedf2840747c892fe7ad090b69
7
- data.tar.gz: 2ec3f3c88c851e86e7ef6485c7719a271dbda81717990446900fe121844daac905b5f554833b59769a4c6f626bb50579d5e51970e7a306d206c898d0a289d36c
6
+ metadata.gz: a2f71a448863bf25e229032cb06d3ae7e7ebf1ef50b414e6486bd902ec7f100dad1ae4cb7160ef9c5fa8069f3ff0bcbb3da29d9c16a49f2e347f8d3d8797049a
7
+ data.tar.gz: 4cd747fb8505531a1ab0f1c28c8b3a09a134d544c1834b2f821c220bce2e3b6efce325b73326e23d92891a27ae5ae3cd9a79e9bbfdfa1b45c8f6e490b74d5c9a
data/CHANGELOG.md CHANGED
@@ -6,28 +6,11 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
- ## [0.2.2]
10
-
11
- - Support document links (https://github.com/Shopify/ruby-lsp/pull/195)
12
- - Avoid returning on request blocks (https://github.com/Shopify/ruby-lsp/pull/232)
13
- - Better specify gemspec files (https://github.com/Shopify/ruby-lsp/pull/233)
14
- - Include Kernel instance methods as special methods for semantic highlighting (https://github.com/Shopify/ruby-lsp/pull/231)
15
- - Fix call processing when message is a :call symbol literal (https://github.com/Shopify/ruby-lsp/pull/236)
16
- - Alert users about non auto-correctable diagnostics (https://github.com/Shopify/ruby-lsp/pull/230)
17
- - Let clients pull diagnostics instead of pushing on edits (https://github.com/Shopify/ruby-lsp/pull/242)
18
-
19
- ## [0.2.1]
20
-
21
- - Implement the exit lifecycle request (https://github.com/Shopify/ruby-lsp/pull/198)
22
- - Remove the Sorbet runtime from the gem's default load path (https://github.com/Shopify/ruby-lsp/pull/214)
23
- - Return nil if the document is already formatted (https://github.com/Shopify/ruby-lsp/pull/216)
24
- - Handle nameless keyword rest parameters in semantic highlighting (https://github.com/Shopify/ruby-lsp/pull/222)
25
- - Display a warning on invalid RuboCop configuration (https://github.com/Shopify/ruby-lsp/pull/226)
26
- - Centralize request handling logic in server.rb (https://github.com/Shopify/ruby-lsp/pull/221)
27
- - Fix folding ranges for chained invocations involving an FCall (https://github.com/Shopify/ruby-lsp/pull/223)
28
- - Fix handling of argument fowarding in semantic highlighting (https://github.com/Shopify/ruby-lsp/pull/228)
29
- - Recover from initial syntax errors when opening documents (https://github.com/Shopify/ruby-lsp/pull/224)
30
- - Highlight occurrences and definitions in document highlight (https://github.com/Shopify/ruby-lsp/pull/187)
9
+ ## [0.3.0]
10
+ - Add on type formatting completions (https://github.com/Shopify/ruby-lsp/pull/253)
11
+ - Upgrade syntax_tree requirement to >= 3.4 (https://github.com/Shopify/ruby-lsp/pull/254)
12
+ - Show error message when there's a InfiniteCorrectionLoop exception (https://github.com/Shopify/ruby-lsp/pull/252)
13
+ - Add request cancellation (https://github.com/Shopify/ruby-lsp/pull/243)
31
14
 
32
15
  ## [0.2.0]
33
16
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.2
1
+ 0.3.0
@@ -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: StandardError, 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: StandardError, 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,187 @@
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 < Interrupt; end
11
+
12
+ class Result < T::Struct
13
+ const :response, T.untyped # rubocop:disable Sorbet/ForbidUntypedStructProps
14
+ const :error, T.nilable(StandardError)
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(StandardError))
92
+
93
+ request_time = Benchmark.realtime do
94
+ response = T.must(@handlers[request[:method]]).action.call(request)
95
+ rescue StandardError => e
96
+ error = e
97
+ end
98
+
99
+ Queue::Result.new(response: response, error: error, request_time: request_time)
100
+ end
101
+
102
+ # Finalize a Queue::Result. All IO operations should happen here to avoid any issues with cancelling requests
103
+ sig do
104
+ params(
105
+ result: Result,
106
+ request: T::Hash[Symbol, T.untyped]
107
+ ).void
108
+ end
109
+ def finalize_request(result, request)
110
+ error = result.error
111
+ if error
112
+ T.must(@handlers[request[:method]]).error_handler&.call(error, request)
113
+
114
+ @writer.write(
115
+ id: request[:id],
116
+ error: {
117
+ code: LanguageServer::Protocol::Constant::ErrorCodes::INTERNAL_ERROR,
118
+ message: result.error.inspect,
119
+ data: request.to_json,
120
+ },
121
+ )
122
+ elsif result.response != Handler::VOID
123
+ @writer.write(id: request[:id], result: result.response)
124
+ end
125
+
126
+ request_time = result.request_time
127
+ if request_time
128
+ @writer.write(method: "telemetry/event", params: telemetry_params(request, request_time, result.error))
129
+ end
130
+ end
131
+
132
+ private
133
+
134
+ sig { returns(Thread) }
135
+ def new_worker
136
+ Thread.new do
137
+ # Thread::Queue#pop is thread safe and will wait until an item is available
138
+ while (job = T.let(@job_queue.pop, T.nilable(Job)))
139
+ # The only time when the job is nil is when the queue is closed and we can then terminate the thread
140
+
141
+ request = job.request
142
+ @mutex.synchronize do
143
+ @jobs.delete(request[:id])
144
+ @current_job = job
145
+ end
146
+
147
+ begin
148
+ next if job.cancelled
149
+
150
+ result = execute(request)
151
+ rescue Cancelled
152
+ # We need to return nil to the client even if the request was cancelled
153
+ result = Queue::Result.new(response: nil, error: nil, request_time: nil)
154
+ ensure
155
+ @mutex.synchronize { @current_job = nil }
156
+ finalize_request(result, request) unless result.nil?
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ sig do
163
+ params(
164
+ request: T::Hash[Symbol, T.untyped],
165
+ request_time: Float,
166
+ error: T.nilable(StandardError)
167
+ ).returns(T::Hash[Symbol, T.any(String, Float)])
168
+ end
169
+ def telemetry_params(request, request_time, error)
170
+ uri = request.dig(:params, :textDocument, :uri)
171
+
172
+ params = {
173
+ request: request[:method],
174
+ lspVersion: RubyLsp::VERSION,
175
+ requestTime: request_time,
176
+ }
177
+
178
+ if error
179
+ params[:errorClass] = error.class.name
180
+ params[:errorMessage] = error.message
181
+ end
182
+
183
+ params[:uri] = uri.sub(%r{.*://#{Dir.home}}, "~") if uri
184
+ params
185
+ end
186
+ end
187
+ end
@@ -1,30 +1,78 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "ruby_lsp/requests/support/source_uri"
5
+
4
6
  module RubyLsp
5
7
  module Requests
6
8
  # ![Document link demo](../../misc/document_link.gif)
7
9
  #
8
10
  # The [document link](https://microsoft.github.io/language-server-protocol/specification#textDocument_documentLink)
9
- # makes `# source://PATH_TO_FILE:line` comments in a Ruby/RBI file clickable if the file exists.
11
+ # makes `# source://PATH_TO_FILE#line` comments in a Ruby/RBI file clickable if the file exists.
10
12
  # When the user clicks the link, it'll open that location.
11
13
  #
12
14
  # # Example
13
15
  #
14
16
  # ```ruby
15
- # # source://syntax_tree-3.2.1/lib/syntax_tree.rb:51 <- it will be clickable and will take the user to that location
17
+ # # source://syntax_tree/3.2.1/lib/syntax_tree.rb#51 <- it will be clickable and will take the user to that location
16
18
  # def format(source, maxwidth = T.unsafe(nil))
17
19
  # end
18
20
  # ```
19
21
  class DocumentLink < BaseRequest
20
22
  extend T::Sig
21
23
 
22
- RUBY_ROOT = "RUBY_ROOT"
24
+ GEM_TO_VERSION_MAP = T.let(
25
+ [*::Gem::Specification.default_stubs, *::Gem::Specification.stubs].map! do |s|
26
+ [s.name, s.version.to_s]
27
+ end.to_h.freeze,
28
+ T::Hash[String, String]
29
+ )
30
+
31
+ class << self
32
+ extend T::Sig
33
+
34
+ sig { returns(T::Hash[String, T::Array[String]]) }
35
+ def gem_paths
36
+ @gem_paths ||= T.let(begin
37
+ lookup = {}
38
+
39
+ Gem::Specification.stubs.each do |stub|
40
+ spec = stub.to_spec
41
+ lookup[spec.name] = {}
42
+ lookup[spec.name][spec.version.to_s] = {}
43
+
44
+ Dir.glob("**/*.rb", base: "#{spec.full_gem_path}/").each do |path|
45
+ lookup[spec.name][spec.version.to_s][path] = "#{spec.full_gem_path}/#{path}"
46
+ end
47
+ end
23
48
 
24
- sig { params(document: Document).void }
25
- def initialize(document)
26
- super
49
+ Gem::Specification.default_stubs.each do |stub|
50
+ spec = stub.to_spec
51
+ lookup[spec.name] = {}
52
+ lookup[spec.name][spec.version.to_s] = {}
53
+ prefix_matchers = [//]
54
+ prefix_matchers.concat(spec.require_paths.map { |rp| Regexp.new("^#{rp}/") })
55
+ prefix_matcher = Regexp.union(prefix_matchers)
56
+
57
+ spec.files.each do |file|
58
+ path = file.sub(prefix_matcher, "")
59
+ lookup[spec.name][spec.version.to_s][path] = "#{RbConfig::CONFIG["rubylibdir"]}/#{path}"
60
+ end
61
+ end
62
+
63
+ lookup
64
+ end, T.nilable(T::Hash[String, T::Array[String]]))
65
+ end
66
+ end
27
67
 
68
+ sig { params(uri: String, document: Document).void }
69
+ def initialize(uri, document)
70
+ super(document)
71
+
72
+ # Match the version based on the version in the RBI file name. Notice that the `@` symbol is sanitized to `%40`
73
+ # in the URI
74
+ version_match = /(?<=%40)[\d.]+(?=\.rbi$)/.match(uri)
75
+ @gem_version = T.let(version_match && version_match[0], T.nilable(String))
28
76
  @links = T.let([], T::Array[LanguageServer::Protocol::Interface::DocumentLink])
29
77
  end
30
78
 
@@ -36,24 +84,36 @@ module RubyLsp
36
84
 
37
85
  sig { params(node: SyntaxTree::Comment).void }
38
86
  def visit_comment(node)
39
- match = node.value.match(%r{source://(?<path>.*):(?<line>\d+)$})
87
+ match = node.value.match(%r{source://.*#\d+$})
40
88
  return unless match
41
89
 
42
- file_path = if match[:path].start_with?(RUBY_ROOT)
43
- match[:path].sub(RUBY_ROOT, RbConfig::CONFIG["rubylibdir"])
44
- else
45
- File.join(Bundler.bundle_path, "gems", match[:path])
46
- end
47
- return unless File.exist?(file_path)
48
-
49
- target = "file://#{file_path}##{match[:line]}"
90
+ uri = T.cast(URI(match[0]), URI::Source)
91
+ gem_version = resolve_version(uri)
92
+ file_path = self.class.gem_paths.dig(uri.gem_name, gem_version, uri.path)
93
+ return if file_path.nil?
50
94
 
51
95
  @links << LanguageServer::Protocol::Interface::DocumentLink.new(
52
96
  range: range_from_syntax_tree_node(node),
53
- target: target,
54
- tooltip: "Jump to #{target.delete_prefix("file://")}"
97
+ target: "file://#{file_path}##{uri.line_number}",
98
+ tooltip: "Jump to #{file_path}##{uri.line_number}"
55
99
  )
56
100
  end
101
+
102
+ private
103
+
104
+ # Try to figure out the gem version for a source:// link. The order of precedence is:
105
+ # 1. The version in the URI
106
+ # 2. The version in the RBI file name
107
+ # 3. The version from the gemspec
108
+ sig { params(uri: URI::Source).returns(T.nilable(String)) }
109
+ def resolve_version(uri)
110
+ version = uri.gem_version
111
+ return version unless version.nil? || version.empty?
112
+
113
+ return @gem_version unless @gem_version.nil? || @gem_version.empty?
114
+
115
+ GEM_TO_VERSION_MAP[uri.gem_name]
116
+ end
57
117
  end
58
118
  end
59
119
  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 }
@@ -0,0 +1,135 @@
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
+ return unless @document.syntax_errors?
45
+
46
+ case @trigger_character
47
+ when "{"
48
+ handle_curly_brace
49
+ when "|"
50
+ handle_pipe
51
+ when "\n"
52
+ handle_statement_end
53
+ end
54
+
55
+ @edits
56
+ end
57
+
58
+ private
59
+
60
+ sig { void }
61
+ def handle_pipe
62
+ return unless /".*|/.match?(@previous_line)
63
+
64
+ add_edit_with_text("|")
65
+ move_cursor_to(@position[:line], @position[:character])
66
+ end
67
+
68
+ sig { void }
69
+ def handle_curly_brace
70
+ return unless /".*#\{/.match?(@previous_line)
71
+
72
+ add_edit_with_text("}")
73
+ move_cursor_to(@position[:line], @position[:character])
74
+ end
75
+
76
+ sig { void }
77
+ def handle_statement_end
78
+ return unless END_REGEXES.any? { |regex| regex.match?(@previous_line) }
79
+
80
+ indents = " " * @indentation
81
+
82
+ add_edit_with_text(" \n#{indents}end")
83
+ move_cursor_to(@position[:line], @indentation + 2)
84
+ end
85
+
86
+ sig { params(text: String).void }
87
+ def add_edit_with_text(text)
88
+ position = Interface::Position.new(
89
+ line: @position[:line],
90
+ character: @position[:character]
91
+ )
92
+
93
+ @edits << Interface::TextEdit.new(
94
+ range: Interface::Range.new(
95
+ start: position,
96
+ end: position
97
+ ),
98
+ new_text: text
99
+ )
100
+ end
101
+
102
+ sig { params(line: Integer, character: Integer).void }
103
+ def move_cursor_to(line, character)
104
+ position = Interface::Position.new(
105
+ line: line,
106
+ character: character
107
+ )
108
+
109
+ # The $0 is a special snippet anchor that moves the cursor to that given position. See the snippets
110
+ # documentation for more information:
111
+ # https://code.visualstudio.com/docs/editor/userdefinedsnippets#_create-your-own-snippets
112
+ @edits << Interface::TextEdit.new(
113
+ range: Interface::Range.new(
114
+ start: position,
115
+ end: position
116
+ ),
117
+ new_text: "$0"
118
+ )
119
+ end
120
+
121
+ sig { params(line: String).returns(Integer) }
122
+ def find_indentation(line)
123
+ count = 0
124
+
125
+ line.chars.each do |c|
126
+ break unless c == " "
127
+
128
+ count += 1
129
+ end
130
+
131
+ count
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,82 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "uri/file"
5
+
6
+ module URI
7
+ # Must be kept in sync with the one in Tapioca
8
+ class Source < URI::File
9
+ extend T::Sig
10
+
11
+ COMPONENT = T.let([
12
+ :scheme,
13
+ :gem_name,
14
+ :gem_version,
15
+ :path,
16
+ :line_number,
17
+ ].freeze, T::Array[Symbol])
18
+
19
+ T.unsafe(self).alias_method(:gem_name, :host)
20
+ T.unsafe(self).alias_method(:line_number, :fragment)
21
+
22
+ sig { returns(T.nilable(String)) }
23
+ attr_reader :gem_version
24
+
25
+ class << self
26
+ extend T::Sig
27
+
28
+ sig do
29
+ params(
30
+ gem_name: String,
31
+ gem_version: T.nilable(String),
32
+ path: String,
33
+ line_number: T.nilable(String)
34
+ ).returns(URI::Source)
35
+ end
36
+ def build(gem_name:, gem_version:, path:, line_number:)
37
+ super(
38
+ {
39
+ scheme: "source",
40
+ host: gem_name,
41
+ path: DEFAULT_PARSER.escape("/#{gem_version}/#{path}"),
42
+ fragment: line_number,
43
+ }
44
+ )
45
+ end
46
+ end
47
+
48
+ sig { params(v: T.nilable(String)).void }
49
+ def set_path(v) # rubocop:disable Naming/AccessorMethodName
50
+ return if v.nil?
51
+
52
+ gem_version, path = v.delete_prefix("/").split("/", 2)
53
+
54
+ @gem_version = T.let(gem_version, T.nilable(String))
55
+ @path = T.let(path, T.nilable(String))
56
+ end
57
+
58
+ sig { params(v: T.nilable(String)).returns(T::Boolean) }
59
+ def check_host(v)
60
+ return true unless v
61
+
62
+ if /[A-Za-z][A-Za-z0-9\-_]*/ !~ v
63
+ raise InvalidComponentError,
64
+ "bad component(expected gem name): #{v}"
65
+ end
66
+
67
+ true
68
+ end
69
+
70
+ sig { returns(String) }
71
+ def to_s
72
+ "source://#{gem_name}/#{gem_version}#{path}##{line_number}"
73
+ end
74
+
75
+ if URI.respond_to?(:register_scheme)
76
+ URI.register_scheme("SOURCE", self)
77
+ else
78
+ @@schemes = T.let(@@schemes, T::Hash[String, Class]) # rubocop:disable Style/ClassVars
79
+ @@schemes["SOURCE"] = self
80
+ end
81
+ end
82
+ 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,6 +48,13 @@ module RubyLsp
48
48
  }
49
49
  end
50
50
 
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
+
51
58
  # TODO: switch back to using Interface::ServerCapabilities once the gem is updated for spec 3.17
52
59
  Interface::InitializeResult.new(
53
60
  capabilities: {
@@ -63,6 +70,7 @@ module RubyLsp
63
70
  documentFormattingProvider: enabled_features.include?("formatting"),
64
71
  documentHighlightProvider: enabled_features.include?("documentHighlights"),
65
72
  codeActionProvider: enabled_features.include?("codeActions"),
73
+ documentOnTypeFormattingProvider: on_type_formatting_provider,
66
74
  diagnosticProvider: diagnostics_provider,
67
75
  }.reject { |_, v| !v }
68
76
  )
@@ -91,25 +99,26 @@ module RubyLsp
91
99
  Handler::VOID
92
100
  end
93
101
 
94
- on("textDocument/documentSymbol") do |request|
102
+ on("textDocument/documentSymbol", parallel: true) do |request|
95
103
  store.cache_fetch(request.dig(:params, :textDocument, :uri), :document_symbol) do |document|
96
104
  Requests::DocumentSymbol.new(document).run
97
105
  end
98
106
  end
99
107
 
100
- on("textDocument/documentLink") do |request|
101
- store.cache_fetch(request.dig(:params, :textDocument, :uri), :document_link) do |document|
102
- RubyLsp::Requests::DocumentLink.new(document).run
108
+ on("textDocument/documentLink", parallel: true) do |request|
109
+ uri = request.dig(:params, :textDocument, :uri)
110
+ store.cache_fetch(uri, :document_link) do |document|
111
+ RubyLsp::Requests::DocumentLink.new(uri, document).run
103
112
  end
104
113
  end
105
114
 
106
- on("textDocument/foldingRange") do |request|
115
+ on("textDocument/foldingRange", parallel: true) do |request|
107
116
  store.cache_fetch(request.dig(:params, :textDocument, :uri), :folding_ranges) do |document|
108
117
  Requests::FoldingRanges.new(document).run
109
118
  end
110
119
  end
111
120
 
112
- on("textDocument/selectionRange") do |request|
121
+ on("textDocument/selectionRange", parallel: true) do |request|
113
122
  uri = request.dig(:params, :textDocument, :uri)
114
123
  positions = request.dig(:params, :positions)
115
124
 
@@ -131,7 +140,7 @@ module RubyLsp
131
140
  end
132
141
  end
133
142
 
134
- on("textDocument/semanticTokens/full") do |request|
143
+ on("textDocument/semanticTokens/full", parallel: true) do |request|
135
144
  store.cache_fetch(request.dig(:params, :textDocument, :uri), :semantic_highlighting) do |document|
136
145
  T.cast(
137
146
  Requests::SemanticHighlighting.new(
@@ -143,13 +152,24 @@ module RubyLsp
143
152
  end
144
153
  end
145
154
 
146
- on("textDocument/formatting") do |request|
155
+ on("textDocument/formatting", parallel: true) do |request|
147
156
  uri = request.dig(:params, :textDocument, :uri)
148
157
 
149
158
  Requests::Formatting.new(uri, store.get(uri)).run
159
+ rescue RuboCop::Runner::InfiniteCorrectionLoop => e
160
+ show_message(Constant::MessageType::ERROR, "Error from RuboCop: #{e.message}")
161
+ nil
162
+ end
163
+
164
+ on("textDocument/onTypeFormatting", parallel: true) do |request|
165
+ uri = request.dig(:params, :textDocument, :uri)
166
+ position = request.dig(:params, :position)
167
+ character = request.dig(:params, :ch)
168
+
169
+ Requests::OnTypeFormatting.new(store.get(uri), position, character).run
150
170
  end
151
171
 
152
- on("textDocument/documentHighlight") do |request|
172
+ on("textDocument/documentHighlight", parallel: true) do |request|
153
173
  document = store.get(request.dig(:params, :textDocument, :uri))
154
174
 
155
175
  if document.parsed?
@@ -157,7 +177,7 @@ module RubyLsp
157
177
  end
158
178
  end
159
179
 
160
- on("textDocument/codeAction") do |request|
180
+ on("textDocument/codeAction", parallel: true) do |request|
161
181
  uri = request.dig(:params, :textDocument, :uri)
162
182
  range = request.dig(:params, :range)
163
183
  start_line = range.dig(:start, :line)
@@ -168,6 +188,11 @@ module RubyLsp
168
188
  end
169
189
  end
170
190
 
191
+ on("$/cancelRequest") do |request|
192
+ cancel_request(request[:params][:id])
193
+ Handler::VOID
194
+ end
195
+
171
196
  on("textDocument/diagnostic") do |request|
172
197
  uri = request.dig(:params, :textDocument, :uri)
173
198
  response = store.cache_fetch(uri, :diagnostics) do |document|
@@ -175,9 +200,10 @@ module RubyLsp
175
200
  end
176
201
 
177
202
  { kind: "full", items: response.map(&:to_lsp_diagnostic) } if response
178
- rescue RuboCop::ValidationError => e
179
- show_message(Constant::MessageType::ERROR, "Error in RuboCop configuration file: #{e.message}")
180
- nil
203
+ end.on_error do |error|
204
+ if error.is_a?(RuboCop::ValidationError)
205
+ show_message(Constant::MessageType::ERROR, "Error in RuboCop configuration file: #{error.message}")
206
+ end
181
207
  end
182
208
 
183
209
  on("shutdown") { shutdown }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-lsp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.0
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-12 00:00:00.000000000 Z
11
+ date: 2022-08-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: language_server-protocol
@@ -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,6 +79,7 @@ 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
@@ -86,6 +88,7 @@ files:
86
88
  - lib/ruby_lsp/requests/support/rubocop_formatting_runner.rb
87
89
  - lib/ruby_lsp/requests/support/selection_range.rb
88
90
  - lib/ruby_lsp/requests/support/semantic_token_encoder.rb
91
+ - lib/ruby_lsp/requests/support/source_uri.rb
89
92
  - lib/ruby_lsp/requests/support/syntax_error_diagnostic.rb
90
93
  - lib/ruby_lsp/server.rb
91
94
  - lib/ruby_lsp/store.rb