ruby-lsp 0.3.8 → 0.4.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: 5f1ed447e51909ada66f6bebceca82d807686a4889468838a797e2ed8afff50e
4
- data.tar.gz: 7b429773bbdebc171debbe446adb868136bd15b5f493542898df0a9c82f3514e
3
+ metadata.gz: 65db08cd80bfe40116f70065b717ad88d1b1fd4fd86c6fcf1de2d5be9636ddce
4
+ data.tar.gz: 84a483aef0e88650a0b2bdbd0db0c3a5c1cd1cfff1e87508a09d7d4d8a616f36
5
5
  SHA512:
6
- metadata.gz: 62e8a6ee36c43dec6ec0af16fd19830b6369446455a5fc16871c368c8bb4ccdb47c0d9c5678990a43604bf03c622c2ef13bb82c6a6e9f26fdcde72b3b18ec99e
7
- data.tar.gz: e441a670c22bcdbfdc3afe99743fe6e1c4aa5f28edaedcf09f47255372322c52ead68988bc6d7ce09170e368e4059c94dac73b395a640c86f20b2969ae8e8095
6
+ metadata.gz: 1ff522bae2c2d469c1fb5817c1b9ed65867ad19d64e00c0b14d4faa6b38bd62321c3109292f1021174766e2da240bd17faea1f8185672164307d2dafe88845bb
7
+ data.tar.gz: 7e89c4c38b833269e9d9a09f55333d32f6e1ef4872174d92e7b92a048cf51f62a837b7d9d4c217f3cccb9a196bb6d3b004439e4de8cc9e588c9b445a2f0c46e8
data/README.md CHANGED
@@ -4,6 +4,37 @@
4
4
 
5
5
  This gem is an implementation of the [language server protocol specification](https://microsoft.github.io/language-server-protocol/) for Ruby, used to improve editor features.
6
6
 
7
+ # Overview
8
+
9
+ The intention of Ruby LSP is to provide a fast, robust and feature-rich coding environment for Ruby developers.
10
+
11
+ It's part of a [wider Shopify goal](https://github.com/Shopify/vscode-shopify-ruby) to provide a state-of-the-art experience to Ruby developers using modern standards for cross-editor features, documentation and debugging.
12
+
13
+ It provides many features, including:
14
+
15
+ * Syntax highlighting
16
+ * Linting and formatting
17
+ * Code folding
18
+ * Selection ranges
19
+
20
+ It does not perform typechecking, so its features are implemented on a best-effort basis, aiming to be as accurate as possible.
21
+
22
+ Planned future features include:
23
+
24
+ * Auto-completion and navigation ("Go To Definition") ([prototype](https://github.com/Shopify/ruby-lsp/pull/429))
25
+ * Support for plug-ins to extend behavior
26
+
27
+ The Ruby LSP does not perform any type-checking or provide any type-related assistance, but it can be used alongside [Sorbet](https://github.com/sorbet/sorbet)'s LSP server.
28
+
29
+ At the time of writing, these are the major differences between Ruby LSP and [Solargraph](https://solargraph.org/):
30
+
31
+ * Solargraph [uses](https://solargraph.org/guides/yard) YARD documentation to gather information about your project and its gem dependencies. This provides functionality such as context-aware auto-completion and navigation ("Go To Definition")
32
+ * Solargraph can be used as a globally installed gem, but Ruby LSP must be added to the Gemfile or gemspec if using RuboCop. (There are pros and cons to each approach)
33
+
34
+ ## Learn More
35
+
36
+ * [RubyConf 2022: Improving the development experience with language servers](https://www.youtube.com/watch?v=kEfXPTm1aCI) ([Vinicius Stock](https://github.com/vinistock))
37
+
7
38
  ## Usage
8
39
 
9
40
  Install the gem. There's no need to require it, since the server is used as a standalone executable.
@@ -17,7 +48,7 @@ end
17
48
  If using VS Code, install the [Ruby LSP extension](https://github.com/Shopify/vscode-ruby-lsp) to get the extra features in
18
49
  the editor.
19
50
 
20
- See the [documentation](https://shopify.github.io/ruby-lsp) for
51
+ See the [documentation](https://shopify.github.io/ruby-lsp) for more in-depth details about the
21
52
  [supported features](https://shopify.github.io/ruby-lsp/RubyLsp/Requests.html).
22
53
 
23
54
  ## Contributing
@@ -105,6 +136,13 @@ The `launch.json` contains a 'Minitest - current file' configuration for the deb
105
136
  1. When the breakpoint is triggered, the process will pause and VS Code will connect to the debugger and activate the debugger UI.
106
137
  1. Open the Debug Console view to use the debugger's REPL.
107
138
 
139
+ ## Spell Checking
140
+
141
+ VS Code users will be prompted to enable the [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) extension.
142
+ By default this will be enabled for all workspaces, but you can choose to selectively enable or disable it per workspace.
143
+
144
+ If you introduce a word which the spell checker does not recognize, you can add it to the `cspell.json` configuration alongside your PR.
145
+
108
146
  ## License
109
147
 
110
148
  The gem is available as open source under the terms of the
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.8
1
+ 0.4.0
data/exe/ruby-lsp CHANGED
@@ -18,4 +18,5 @@ rescue
18
18
  nil
19
19
  end
20
20
 
21
- require_relative "../lib/ruby_lsp/server"
21
+ require_relative "../lib/ruby_lsp/internal"
22
+ RubyLsp::Server.new.start
@@ -0,0 +1,362 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ # This class dispatches a request execution to the right request class. No IO should happen anywhere here!
6
+ class Executor
7
+ extend T::Sig
8
+
9
+ sig { params(store: Store).void }
10
+ def initialize(store)
11
+ # Requests that mutate the store must be run sequentially! Parallel requests only receive a temporary copy of the
12
+ # store
13
+ @store = store
14
+ @notifications = T.let([], T::Array[Notification])
15
+ end
16
+
17
+ sig { params(request: T::Hash[Symbol, T.untyped]).returns(Result) }
18
+ def execute(request)
19
+ response = T.let(nil, T.untyped)
20
+ error = T.let(nil, T.nilable(Exception))
21
+
22
+ request_time = Benchmark.realtime do
23
+ response = run(request)
24
+ rescue StandardError, LoadError => e
25
+ error = e
26
+ end
27
+
28
+ Result.new(response: response, error: error, request_time: request_time, notifications: @notifications)
29
+ end
30
+
31
+ private
32
+
33
+ sig { params(request: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
34
+ def run(request)
35
+ uri = request.dig(:params, :textDocument, :uri)
36
+
37
+ case request[:method]
38
+ when "initialize"
39
+ initialize_request(request.dig(:params))
40
+ when "textDocument/didOpen"
41
+ text_document_did_open(uri, request.dig(:params, :textDocument, :text))
42
+ when "textDocument/didClose"
43
+ @notifications << Notification.new(
44
+ message: "textDocument/publishDiagnostics",
45
+ params: Interface::PublishDiagnosticsParams.new(uri: uri, diagnostics: []),
46
+ )
47
+
48
+ text_document_did_close(uri)
49
+ when "textDocument/didChange"
50
+ text_document_did_change(uri, request.dig(:params, :contentChanges))
51
+ when "textDocument/foldingRange"
52
+ folding_range(uri)
53
+ when "textDocument/documentLink"
54
+ document_link(uri)
55
+ when "textDocument/selectionRange"
56
+ selection_range(uri, request.dig(:params, :positions))
57
+ when "textDocument/documentSymbol"
58
+ document_symbol(uri)
59
+ when "textDocument/semanticTokens/full"
60
+ semantic_tokens_full(uri)
61
+ when "textDocument/semanticTokens/range"
62
+ semantic_tokens_range(uri, request.dig(:params, :range))
63
+ when "textDocument/formatting"
64
+ begin
65
+ formatting(uri)
66
+ rescue StandardError => error
67
+ @notifications << Notification.new(
68
+ message: "window/showMessage",
69
+ params: Interface::ShowMessageParams.new(
70
+ type: Constant::MessageType::ERROR,
71
+ message: "Formatting error: #{error.message}",
72
+ ),
73
+ )
74
+
75
+ nil
76
+ end
77
+ when "textDocument/documentHighlight"
78
+ document_highlight(uri, request.dig(:params, :position))
79
+ when "textDocument/onTypeFormatting"
80
+ on_type_formatting(uri, request.dig(:params, :position), request.dig(:params, :ch))
81
+ when "textDocument/hover"
82
+ hover(uri, request.dig(:params, :position))
83
+ when "textDocument/inlayHint"
84
+ inlay_hint(uri, request.dig(:params, :range))
85
+ when "textDocument/codeAction"
86
+ code_action(uri, request.dig(:params, :range), request.dig(:params, :context))
87
+ when "textDocument/diagnostic"
88
+ begin
89
+ diagnostic(uri)
90
+ rescue StandardError => error
91
+ @notifications << Notification.new(
92
+ message: "window/showMessage",
93
+ params: Interface::ShowMessageParams.new(
94
+ type: Constant::MessageType::ERROR,
95
+ message: "Error running diagnostics: #{error.message}",
96
+ ),
97
+ )
98
+
99
+ nil
100
+ end
101
+ when "textDocument/completion"
102
+ completion(uri, request.dig(:params, :position))
103
+ end
104
+ end
105
+
106
+ sig { params(uri: String).returns(T::Array[Interface::FoldingRange]) }
107
+ def folding_range(uri)
108
+ @store.cache_fetch(uri, :folding_ranges) do |document|
109
+ Requests::FoldingRanges.new(document).run
110
+ end
111
+ end
112
+
113
+ sig do
114
+ params(
115
+ uri: String,
116
+ position: Document::PositionShape,
117
+ ).returns(T.nilable(Interface::Hover))
118
+ end
119
+ def hover(uri, position)
120
+ RubyLsp::Requests::Hover.new(@store.get(uri), position).run
121
+ end
122
+
123
+ sig { params(uri: String).returns(T::Array[Interface::DocumentLink]) }
124
+ def document_link(uri)
125
+ @store.cache_fetch(uri, :document_link) do |document|
126
+ RubyLsp::Requests::DocumentLink.new(uri, document).run
127
+ end
128
+ end
129
+
130
+ sig { params(uri: String).returns(T::Array[Interface::DocumentSymbol]) }
131
+ def document_symbol(uri)
132
+ @store.cache_fetch(uri, :document_symbol) do |document|
133
+ Requests::DocumentSymbol.new(document).run
134
+ end
135
+ end
136
+
137
+ sig { params(uri: String, content_changes: T::Array[Document::EditShape]).returns(Object) }
138
+ def text_document_did_change(uri, content_changes)
139
+ @store.push_edits(uri, content_changes)
140
+ VOID
141
+ end
142
+
143
+ sig { params(uri: String, text: String).returns(Object) }
144
+ def text_document_did_open(uri, text)
145
+ @store.set(uri, text)
146
+ VOID
147
+ end
148
+
149
+ sig { params(uri: String).returns(Object) }
150
+ def text_document_did_close(uri)
151
+ @store.delete(uri)
152
+ VOID
153
+ end
154
+
155
+ sig do
156
+ params(
157
+ uri: String,
158
+ positions: T::Array[Document::PositionShape],
159
+ ).returns(T.nilable(T::Array[T.nilable(Requests::Support::SelectionRange)]))
160
+ end
161
+ def selection_range(uri, positions)
162
+ ranges = @store.cache_fetch(uri, :selection_ranges) do |document|
163
+ Requests::SelectionRanges.new(document).run
164
+ end
165
+
166
+ # Per the selection range request spec (https://microsoft.github.io/language-server-protocol/specification#textDocument_selectionRange),
167
+ # every position in the positions array should have an element at the same index in the response
168
+ # array. For positions without a valid selection range, the corresponding element in the response
169
+ # array will be nil.
170
+
171
+ unless ranges.nil?
172
+ positions.map do |position|
173
+ ranges.find do |range|
174
+ range.cover?(position)
175
+ end
176
+ end
177
+ end
178
+ end
179
+
180
+ sig { params(uri: String).returns(Interface::SemanticTokens) }
181
+ def semantic_tokens_full(uri)
182
+ @store.cache_fetch(uri, :semantic_highlighting) do |document|
183
+ T.cast(
184
+ Requests::SemanticHighlighting.new(
185
+ document,
186
+ encoder: Requests::Support::SemanticTokenEncoder.new,
187
+ ).run,
188
+ Interface::SemanticTokens,
189
+ )
190
+ end
191
+ end
192
+
193
+ sig { params(uri: String).returns(T.nilable(T::Array[Interface::TextEdit])) }
194
+ def formatting(uri)
195
+ Requests::Formatting.new(uri, @store.get(uri)).run
196
+ end
197
+
198
+ sig do
199
+ params(
200
+ uri: String,
201
+ position: Document::PositionShape,
202
+ character: String,
203
+ ).returns(T::Array[Interface::TextEdit])
204
+ end
205
+ def on_type_formatting(uri, position, character)
206
+ Requests::OnTypeFormatting.new(@store.get(uri), position, character).run
207
+ end
208
+
209
+ sig do
210
+ params(
211
+ uri: String,
212
+ position: Document::PositionShape,
213
+ ).returns(T::Array[Interface::DocumentHighlight])
214
+ end
215
+ def document_highlight(uri, position)
216
+ Requests::DocumentHighlight.new(@store.get(uri), position).run
217
+ end
218
+
219
+ sig { params(uri: String, range: Document::RangeShape).returns(T::Array[Interface::InlayHint]) }
220
+ def inlay_hint(uri, range)
221
+ document = @store.get(uri)
222
+ start_line = range.dig(:start, :line)
223
+ end_line = range.dig(:end, :line)
224
+
225
+ Requests::InlayHints.new(document, start_line..end_line).run
226
+ end
227
+
228
+ sig do
229
+ params(
230
+ uri: String,
231
+ range: Document::RangeShape,
232
+ context: T::Hash[Symbol, T.untyped],
233
+ ).returns(T.nilable(T::Array[Interface::CodeAction]))
234
+ end
235
+ def code_action(uri, range, context)
236
+ start_line = range.dig(:start, :line)
237
+ end_line = range.dig(:end, :line)
238
+ document = @store.get(uri)
239
+
240
+ Requests::CodeActions.new(uri, document, start_line..end_line, context).run
241
+ end
242
+
243
+ sig { params(uri: String).returns(T.nilable(Interface::FullDocumentDiagnosticReport)) }
244
+ def diagnostic(uri)
245
+ response = @store.cache_fetch(uri, :diagnostics) do |document|
246
+ Requests::Diagnostics.new(uri, document).run
247
+ end
248
+
249
+ Interface::FullDocumentDiagnosticReport.new(kind: "full", items: response.map(&:to_lsp_diagnostic)) if response
250
+ end
251
+
252
+ sig { params(uri: String, range: Document::RangeShape).returns(Interface::SemanticTokens) }
253
+ def semantic_tokens_range(uri, range)
254
+ document = @store.get(uri)
255
+ start_line = range.dig(:start, :line)
256
+ end_line = range.dig(:end, :line)
257
+
258
+ T.cast(
259
+ Requests::SemanticHighlighting.new(
260
+ document,
261
+ range: start_line..end_line,
262
+ encoder: Requests::Support::SemanticTokenEncoder.new,
263
+ ).run,
264
+ Interface::SemanticTokens,
265
+ )
266
+ end
267
+
268
+ sig do
269
+ params(uri: String, position: Document::PositionShape).returns(T.nilable(T::Array[Interface::CompletionItem]))
270
+ end
271
+ def completion(uri, position)
272
+ Requests::PathCompletion.new(@store.get(uri), position).run
273
+ end
274
+
275
+ sig { params(options: T::Hash[Symbol, T.untyped]).returns(Interface::InitializeResult) }
276
+ def initialize_request(options)
277
+ @store.clear
278
+ @store.encoding = options.dig(:capabilities, :general, :positionEncodings)
279
+ enabled_features = options.dig(:initializationOptions, :enabledFeatures) || []
280
+
281
+ document_symbol_provider = if enabled_features.include?("documentSymbols")
282
+ Interface::DocumentSymbolClientCapabilities.new(
283
+ hierarchical_document_symbol_support: true,
284
+ symbol_kind: {
285
+ value_set: Requests::DocumentSymbol::SYMBOL_KIND.values,
286
+ },
287
+ )
288
+ end
289
+
290
+ document_link_provider = if enabled_features.include?("documentLink")
291
+ Interface::DocumentLinkOptions.new(resolve_provider: false)
292
+ end
293
+
294
+ hover_provider = if enabled_features.include?("hover")
295
+ Interface::HoverClientCapabilities.new(dynamic_registration: false)
296
+ end
297
+
298
+ folding_ranges_provider = if enabled_features.include?("foldingRanges")
299
+ Interface::FoldingRangeClientCapabilities.new(line_folding_only: true)
300
+ end
301
+
302
+ semantic_tokens_provider = if enabled_features.include?("semanticHighlighting")
303
+ Interface::SemanticTokensRegistrationOptions.new(
304
+ document_selector: { scheme: "file", language: "ruby" },
305
+ legend: Interface::SemanticTokensLegend.new(
306
+ token_types: Requests::SemanticHighlighting::TOKEN_TYPES.keys,
307
+ token_modifiers: Requests::SemanticHighlighting::TOKEN_MODIFIERS.keys,
308
+ ),
309
+ range: true,
310
+ full: { delta: false },
311
+ )
312
+ end
313
+
314
+ diagnostics_provider = if enabled_features.include?("diagnostics")
315
+ {
316
+ interFileDependencies: false,
317
+ workspaceDiagnostics: false,
318
+ }
319
+ end
320
+
321
+ on_type_formatting_provider = if enabled_features.include?("onTypeFormatting")
322
+ Interface::DocumentOnTypeFormattingOptions.new(
323
+ first_trigger_character: "{",
324
+ more_trigger_character: ["\n", "|"],
325
+ )
326
+ end
327
+
328
+ inlay_hint_provider = if enabled_features.include?("inlayHint")
329
+ Interface::InlayHintOptions.new(resolve_provider: false)
330
+ end
331
+
332
+ completion_provider = if enabled_features.include?("completion")
333
+ Interface::CompletionOptions.new(
334
+ resolve_provider: false,
335
+ trigger_characters: ["/"],
336
+ )
337
+ end
338
+
339
+ Interface::InitializeResult.new(
340
+ capabilities: Interface::ServerCapabilities.new(
341
+ text_document_sync: Interface::TextDocumentSyncOptions.new(
342
+ change: Constant::TextDocumentSyncKind::INCREMENTAL,
343
+ open_close: true,
344
+ ),
345
+ selection_range_provider: enabled_features.include?("selectionRanges"),
346
+ hover_provider: hover_provider,
347
+ document_symbol_provider: document_symbol_provider,
348
+ document_link_provider: document_link_provider,
349
+ folding_range_provider: folding_ranges_provider,
350
+ semantic_tokens_provider: semantic_tokens_provider,
351
+ document_formatting_provider: enabled_features.include?("formatting"),
352
+ document_highlight_provider: enabled_features.include?("documentHighlights"),
353
+ code_action_provider: enabled_features.include?("codeActions"),
354
+ document_on_type_formatting_provider: on_type_formatting_provider,
355
+ diagnostic_provider: diagnostics_provider,
356
+ inlay_hint_provider: inlay_hint_provider,
357
+ completion_provider: completion_provider,
358
+ ),
359
+ )
360
+ end
361
+ end
362
+ end
@@ -4,6 +4,11 @@
4
4
  require "sorbet-runtime"
5
5
  require "syntax_tree"
6
6
  require "language_server-protocol"
7
+ require "benchmark"
7
8
 
8
9
  require "ruby-lsp"
9
- require "ruby_lsp/handler"
10
+ require "ruby_lsp/utils"
11
+ require "ruby_lsp/server"
12
+ require "ruby_lsp/executor"
13
+ require "ruby_lsp/requests"
14
+ require "ruby_lsp/store"
@@ -10,8 +10,12 @@ module RubyLsp
10
10
 
11
11
  abstract!
12
12
 
13
- sig { params(document: Document).void }
14
- def initialize(document)
13
+ # We must accept rest keyword arguments here, so that the argument count matches when
14
+ # SyntaxTree::WithScope#initialize invokes `super` for Sorbet. We don't actually use these parameters for
15
+ # anything. We can remove these arguments once we drop support for Ruby 2.7
16
+ # https://github.com/ruby-syntax-tree/syntax_tree/blob/4dac90b53df388f726dce50ce638a1ba71cc59f8/lib/syntax_tree/with_scope.rb#L122
17
+ sig { params(document: Document, _kwargs: T.untyped).void }
18
+ def initialize(document, **_kwargs)
15
19
  @document = document
16
20
 
17
21
  # Parsing the document here means we're taking a lazy approach by only doing it when the first feature request
@@ -62,9 +66,10 @@ module RubyLsp
62
66
  params(
63
67
  node: SyntaxTree::Node,
64
68
  position: Integer,
69
+ node_types: T::Array[T.class_of(SyntaxTree::Node)],
65
70
  ).returns([T.nilable(SyntaxTree::Node), T.nilable(SyntaxTree::Node)])
66
71
  end
67
- def locate(node, position)
72
+ def locate(node, position, node_types: [])
68
73
  queue = T.let(node.child_nodes.compact, T::Array[T.nilable(SyntaxTree::Node)])
69
74
  closest = node
70
75
 
@@ -90,6 +95,8 @@ module RubyLsp
90
95
  parent = T.let(closest, SyntaxTree::Node)
91
96
  closest = candidate
92
97
  end
98
+
99
+ break if node_types.any? { |type| candidate.is_a?(type) }
93
100
  end
94
101
 
95
102
  [closest, parent]
@@ -24,24 +24,38 @@ module RubyLsp
24
24
  uri: String,
25
25
  document: Document,
26
26
  range: T::Range[Integer],
27
+ context: T::Hash[Symbol, T.untyped],
27
28
  ).void
28
29
  end
29
- def initialize(uri, document, range)
30
+ def initialize(uri, document, range, context)
30
31
  super(document)
31
32
 
32
33
  @uri = uri
33
34
  @range = range
35
+ @context = context
34
36
  end
35
37
 
36
- sig { override.returns(T.all(T::Array[LanguageServer::Protocol::Interface::CodeAction], Object)) }
38
+ sig { override.returns(T.nilable(T.all(T::Array[Interface::CodeAction], Object))) }
37
39
  def run
38
- diagnostics = @document.cache_fetch(:diagnostics) { Diagnostics.new(@uri, @document).run }
39
- corrections = diagnostics.select do |diagnostic|
40
- diagnostic.correctable? && T.cast(diagnostic, Support::RuboCopDiagnostic).in_range?(@range)
40
+ diagnostics = @context[:diagnostics]
41
+ return if diagnostics.nil? || diagnostics.empty?
42
+
43
+ diagnostics.filter_map do |diagnostic|
44
+ code_action = diagnostic.dig(:data, :code_action)
45
+ next if code_action.nil?
46
+
47
+ # We want to return only code actions that are within range or that do not have any edits, such as refactor
48
+ # code actions
49
+ range = code_action.dig(:edit, :documentChanges, 0, :edits, 0, :range)
50
+ code_action if diagnostic.dig(:data, :correctable) && cover?(range)
41
51
  end
42
- return [] if corrections.empty?
52
+ end
53
+
54
+ private
43
55
 
44
- T.cast(corrections, T::Array[Support::RuboCopDiagnostic]).map!(&:to_lsp_code_action)
56
+ sig { params(range: T.nilable(Document::RangeShape)).returns(T::Boolean) }
57
+ def cover?(range)
58
+ range.nil? || @range.cover?(range.dig(:start, :line)..range.dig(:end, :line))
45
59
  end
46
60
  end
47
61
  end
@@ -15,7 +15,7 @@ module RubyLsp
15
15
  #
16
16
  # ```ruby
17
17
  # def say_hello
18
- # puts "Hello" # --> diagnostics: incorrect indentantion
18
+ # puts "Hello" # --> diagnostics: incorrect indentation
19
19
  # end
20
20
  # ```
21
21
  class Diagnostics < BaseRequest
@@ -28,17 +28,13 @@ module RubyLsp
28
28
  @uri = uri
29
29
  end
30
30
 
31
- sig do
32
- override.returns(
33
- T.any(
34
- T.all(T::Array[Support::RuboCopDiagnostic], Object),
35
- T.all(T::Array[Support::SyntaxErrorDiagnostic], Object),
36
- ),
37
- )
38
- end
31
+ sig { override.returns(T.nilable(T.all(T::Array[Support::RuboCopDiagnostic], Object))) }
39
32
  def run
40
- return [] if @document.syntax_error?
41
- return [] unless defined?(Support::RuboCopDiagnosticsRunner)
33
+ return if @document.syntax_error?
34
+ return unless defined?(Support::RuboCopDiagnosticsRunner)
35
+
36
+ # Don't try to run RuboCop diagnostics for files outside the current working directory
37
+ return unless @uri.sub("file://", "").start_with?(Dir.pwd)
42
38
 
43
39
  Support::RuboCopDiagnosticsRunner.instance.run(@uri, @document)
44
40
  end
@@ -32,8 +32,11 @@ module RubyLsp
32
32
  @uri = uri
33
33
  end
34
34
 
35
- sig { override.returns(T.nilable(T.all(T::Array[LanguageServer::Protocol::Interface::TextEdit], Object))) }
35
+ sig { override.returns(T.nilable(T.all(T::Array[Interface::TextEdit], Object))) }
36
36
  def run
37
+ # Don't try to format files outside the current working directory
38
+ return unless @uri.sub("file://", "").start_with?(Dir.pwd)
39
+
37
40
  formatted_text = formatted_file
38
41
  return unless formatted_text
39
42
 
@@ -41,10 +44,10 @@ module RubyLsp
41
44
  return if formatted_text.size == size && formatted_text == @document.source
42
45
 
43
46
  [
44
- LanguageServer::Protocol::Interface::TextEdit.new(
45
- range: LanguageServer::Protocol::Interface::Range.new(
46
- start: LanguageServer::Protocol::Interface::Position.new(line: 0, character: 0),
47
- end: LanguageServer::Protocol::Interface::Position.new(line: size, character: size),
47
+ Interface::TextEdit.new(
48
+ range: Interface::Range.new(
49
+ start: Interface::Position.new(line: 0, character: 0),
50
+ end: Interface::Position.new(line: size, character: size),
48
51
  ),
49
52
  new_text: formatted_text,
50
53
  ),
@@ -32,8 +32,8 @@ module RubyLsp
32
32
 
33
33
  scanner = document.create_scanner
34
34
  line_begin = position[:line] == 0 ? 0 : scanner.find_char_position({ line: position[:line] - 1, character: 0 })
35
- line_end = scanner.find_char_position(position)
36
- line = T.must(@document.source[line_begin..line_end])
35
+ @line_end = T.let(scanner.find_char_position(position), Integer)
36
+ line = T.must(@document.source[line_begin..@line_end])
37
37
 
38
38
  @indentation = T.let(find_indentation(line), Integer)
39
39
  @previous_line = T.let(line.strip.chomp, String)
@@ -42,7 +42,7 @@ module RubyLsp
42
42
  @trigger_character = trigger_character
43
43
  end
44
44
 
45
- sig { override.returns(T.nilable(T.all(T::Array[Interface::TextEdit], Object))) }
45
+ sig { override.returns(T.all(T::Array[Interface::TextEdit], Object)) }
46
46
  def run
47
47
  case @trigger_character
48
48
  when "{"
@@ -84,8 +84,30 @@ module RubyLsp
84
84
 
85
85
  indents = " " * @indentation
86
86
 
87
- add_edit_with_text(" \n#{indents}end")
88
- move_cursor_to(@position[:line], @indentation + 2)
87
+ if @previous_line.include?("\n")
88
+ # If the previous line has a line break, then it means there's content after the line break that triggered
89
+ # this completion. For these cases, we want to add the `end` after the content and move the cursor back to the
90
+ # keyword that triggered the completion
91
+
92
+ line = @position[:line]
93
+
94
+ # If there are enough lines in the document, we want to add the `end` token on the line below the extra
95
+ # content. Otherwise, we want to insert and extra line break ourselves
96
+ correction = if T.must(@document.source[@line_end..-1]).count("\n") >= 2
97
+ line -= 1
98
+ "#{indents}end"
99
+ else
100
+ "#{indents}\nend"
101
+ end
102
+
103
+ add_edit_with_text(correction, { line: @position[:line] + 1, character: @position[:character] })
104
+ move_cursor_to(line, @indentation + 3)
105
+ else
106
+ # If there's nothing after the new line break that triggered the completion, then we want to add the `end` and
107
+ # move the cursor to the body of the statement
108
+ add_edit_with_text(" \n#{indents}end")
109
+ move_cursor_to(@position[:line], @indentation + 2)
110
+ end
89
111
  end
90
112
 
91
113
  sig { params(spaces: String).void }
@@ -94,18 +116,15 @@ module RubyLsp
94
116
  move_cursor_to(@position[:line], @indentation + spaces.size + 1)
95
117
  end
96
118
 
97
- sig { params(text: String).void }
98
- def add_edit_with_text(text)
99
- position = Interface::Position.new(
100
- line: @position[:line],
101
- character: @position[:character],
119
+ sig { params(text: String, position: Document::PositionShape).void }
120
+ def add_edit_with_text(text, position = @position)
121
+ pos = Interface::Position.new(
122
+ line: position[:line],
123
+ character: position[:character],
102
124
  )
103
125
 
104
126
  @edits << Interface::TextEdit.new(
105
- range: Interface::Range.new(
106
- start: position,
107
- end: position,
108
- ),
127
+ range: Interface::Range.new(start: pos, end: pos),
109
128
  new_text: text,
110
129
  )
111
130
  end