ruby-lsp 0.6.2 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0da1b154c2c048cdea8d1ca6f87e77b895482cdbea155058550eff983fdbae1c
4
- data.tar.gz: 7a67502b28e7e525acc6349fafe5de9e9945350d84af72d0a9afaa5d5b0803d8
3
+ metadata.gz: a0761510f35a450cb8ba4b2875dc50826abdc12c07158cd9ee442c863dae5292
4
+ data.tar.gz: 7906c0f49b975b75f09c96e0d28222f344963eca363a6a08b29ba0aa697b2084
5
5
  SHA512:
6
- metadata.gz: 77968226990bdcbab7026e962a3b7e1e3459f920939aa19a50331daa7b4a24b180eb46a342b5385412a43b73d1d6e40d6f9de2f1321a512cf65d36046b978cbc
7
- data.tar.gz: a27e182f7c554d9b231f4ea69b15b52d0c4c32fe0cbb17879480c8b9ada87288c59ffa93fbfd99963c456bf4a6298164c920f720611532609f5e985e3c1e890c
6
+ metadata.gz: aaf45a911c54ce8a3e2e0051beb5941016b689a2e13bbbf2d438cda04ce315560d0af9f206cd2ee8bb136989dbd6863068c5d4c5d52d5b9372584beda7973815
7
+ data.tar.gz: 828569fe84d0e0ff5cf714b194926c07f30ae58cc23bf8e31bed1aa3b930406c9670561631e8c7ff9f151d01a12dc321c3ca85979923dfb6e88023fa7cab1528
data/README.md CHANGED
@@ -27,7 +27,11 @@ The gem can be installed by doing
27
27
  gem install ruby-lsp
28
28
  ```
29
29
 
30
- If you decide to add the gem to the bundle, it is not necessary to require it.
30
+ **NOTE**: starting with v0.7.0, it is no longer recommended to add the `ruby-lsp` to the bundle. The gem will generate a
31
+ custom bundle in `.ruby-lsp/Gemfile` which is used to identify the versions of dependencies that should be used for the
32
+ application (e.g.: the correct RuboCop version).
33
+
34
+ For older versions, if you decide to add the gem to the bundle, it is not necessary to require it.
31
35
  ```ruby
32
36
  group :development do
33
37
  gem "ruby-lsp", require: false
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.6.2
1
+ 0.7.1
data/exe/ruby-lsp CHANGED
@@ -1,6 +1,20 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
+ # When we're running without bundler, then we need to make sure the custom bundle is fully configured and re-execute
5
+ # using `BUNDLE_GEMFILE=.ruby-lsp/Gemfile bundle exec ruby-lsp` so that we have access to the gems that are a part of
6
+ # the application's bundle
7
+ if ENV["BUNDLE_GEMFILE"].nil? && File.exist?("Gemfile.lock")
8
+ require_relative "../lib/ruby_lsp/setup_bundler"
9
+ RubyLsp::SetupBundler.new(Dir.pwd).setup!
10
+
11
+ # In some cases, like when the `ruby-lsp` is already a part of the bundle, we don't generate `.ruby-lsp/Gemfile`.
12
+ # However, we still want to run the server with `bundle exec`. We need to make sure we're pointing to the right
13
+ # `Gemfile`
14
+ bundle_gemfile = File.exist?(".ruby-lsp/Gemfile") ? ".ruby-lsp/Gemfile" : "Gemfile"
15
+ exit exec("BUNDLE_GEMFILE=#{bundle_gemfile} bundle exec ruby-lsp #{ARGV.join(" ")}")
16
+ end
17
+
4
18
  require "sorbet-runtime"
5
19
 
6
20
  begin
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # This executable checks if all automatic LSP requests run successfully on every Ruby file under the current directory
5
+
6
+ require "sorbet-runtime"
7
+
8
+ begin
9
+ T::Configuration.default_checked_level = :never
10
+ T::Configuration.call_validation_error_handler = ->(*) {}
11
+ T::Configuration.inline_type_error_handler = ->(*) {}
12
+ T::Configuration.sig_validation_error_handler = ->(*) {}
13
+ rescue
14
+ nil
15
+ end
16
+
17
+ require_relative "../lib/ruby_lsp/internal"
18
+
19
+ RubyLsp::Extension.load_extensions
20
+
21
+ T::Utils.run_all_sig_blocks
22
+
23
+ files = Dir.glob("#{Dir.pwd}/**/*.rb")
24
+
25
+ puts "Verifying that all automatic LSP requests execute successfully. This may take a while..."
26
+
27
+ errors = {}
28
+ store = RubyLsp::Store.new
29
+ message_queue = Thread::Queue.new
30
+ executor = RubyLsp::Executor.new(store, message_queue)
31
+
32
+ files.each_with_index do |file, index|
33
+ uri = "file://#{file}"
34
+ store.set(uri: uri, source: File.read(file), version: 1)
35
+
36
+ # Executing any of the automatic requests will execute all of them, so here we just pick one
37
+ result = executor.execute({
38
+ method: "textDocument/documentSymbol",
39
+ params: { textDocument: { uri: uri } },
40
+ })
41
+
42
+ error = result.error
43
+ errors[file] = error if error
44
+ ensure
45
+ store.delete(uri)
46
+ print("\033[M\033[0KCompleted #{index + 1}/#{files.length}") unless ENV["CI"]
47
+ end
48
+
49
+ puts "\n"
50
+ message_queue.close
51
+
52
+ if errors.empty?
53
+ puts "All automatic LSP requests executed successfully"
54
+ exit
55
+ end
56
+
57
+ puts <<~ERRORS
58
+ Errors while executing requests:
59
+
60
+ #{errors.map { |file, error| "#{file}: #{error.message}" }.join("\n")}
61
+ ERRORS
62
+ exit!
@@ -114,12 +114,12 @@ module RubyLsp
114
114
  params(
115
115
  position: PositionShape,
116
116
  node_types: T::Array[T.class_of(SyntaxTree::Node)],
117
- ).returns([T.nilable(SyntaxTree::Node), T.nilable(SyntaxTree::Node)])
117
+ ).returns([T.nilable(SyntaxTree::Node), T.nilable(SyntaxTree::Node), T::Array[String]])
118
118
  end
119
119
  def locate_node(position, node_types: [])
120
- return [nil, nil] unless parsed?
120
+ return [nil, nil, []] unless parsed?
121
121
 
122
- locate(T.must(@tree), create_scanner.find_char_position(position))
122
+ locate(T.must(@tree), create_scanner.find_char_position(position), node_types: node_types)
123
123
  end
124
124
 
125
125
  sig do
@@ -127,12 +127,13 @@ module RubyLsp
127
127
  node: SyntaxTree::Node,
128
128
  char_position: Integer,
129
129
  node_types: T::Array[T.class_of(SyntaxTree::Node)],
130
- ).returns([T.nilable(SyntaxTree::Node), T.nilable(SyntaxTree::Node)])
130
+ ).returns([T.nilable(SyntaxTree::Node), T.nilable(SyntaxTree::Node), T::Array[String]])
131
131
  end
132
132
  def locate(node, char_position, node_types: [])
133
133
  queue = T.let(node.child_nodes.compact, T::Array[T.nilable(SyntaxTree::Node)])
134
134
  closest = node
135
135
  parent = T.let(nil, T.nilable(SyntaxTree::Node))
136
+ nesting = T.let([], T::Array[T.any(SyntaxTree::ClassDeclaration, SyntaxTree::ModuleDeclaration)])
136
137
 
137
138
  until queue.empty?
138
139
  candidate = queue.shift
@@ -140,8 +141,10 @@ module RubyLsp
140
141
  # Skip nil child nodes
141
142
  next if candidate.nil?
142
143
 
143
- # Add the next child_nodes to the queue to be processed
144
- queue.concat(candidate.child_nodes)
144
+ # Add the next child_nodes to the queue to be processed. The order here is important! We want to move in the
145
+ # same order as the visiting mechanism, which means searching the child nodes before moving on to the next
146
+ # sibling
147
+ queue.unshift(*candidate.child_nodes)
145
148
 
146
149
  # Skip if the current node doesn't cover the desired position
147
150
  loc = candidate.location
@@ -151,6 +154,17 @@ module RubyLsp
151
154
  # already
152
155
  break if char_position < loc.start_char
153
156
 
157
+ # If the candidate starts after the end of the previous nesting level, then we've exited that nesting level and
158
+ # need to pop the stack
159
+ previous_level = nesting.last
160
+ nesting.pop if previous_level && candidate.start_char > previous_level.end_char
161
+
162
+ # Keep track of the nesting where we found the target. This is used to determine the fully qualified name of the
163
+ # target when it is a constant
164
+ if candidate.is_a?(SyntaxTree::ClassDeclaration) || candidate.is_a?(SyntaxTree::ModuleDeclaration)
165
+ nesting << candidate
166
+ end
167
+
154
168
  # If there are node types to filter by, and the current node is not one of those types, then skip it
155
169
  next if node_types.any? && node_types.none? { |type| candidate.class == type }
156
170
 
@@ -162,7 +176,7 @@ module RubyLsp
162
176
  end
163
177
  end
164
178
 
165
- [closest, parent]
179
+ [closest, parent, nesting.map { |n| n.constant.constant.value }]
166
180
  end
167
181
 
168
182
  class Scanner
@@ -97,13 +97,11 @@ module RubyLsp
97
97
  document_symbol = Requests::DocumentSymbol.new(emitter, @message_queue)
98
98
  document_link = Requests::DocumentLink.new(uri, emitter, @message_queue)
99
99
  code_lens = Requests::CodeLens.new(uri, emitter, @message_queue, @test_library)
100
- code_lens_extensions_listeners = Requests::CodeLens.listeners.map do |l|
101
- T.unsafe(l).new(document.uri, emitter, @message_queue)
102
- end
100
+
103
101
  semantic_highlighting = Requests::SemanticHighlighting.new(emitter, @message_queue)
104
102
  emitter.visit(document.tree) if document.parsed?
105
103
 
106
- code_lens_extensions_listeners.each { |ext| code_lens.merge_response!(ext) }
104
+ code_lens.merge_external_listeners_responses!
107
105
 
108
106
  # Store all responses retrieve in this round of visits in the cache and then return the response for the request
109
107
  # we actually received
@@ -169,9 +167,26 @@ module RubyLsp
169
167
  end
170
168
  when "textDocument/completion"
171
169
  completion(uri, request.dig(:params, :position))
170
+ when "textDocument/definition"
171
+ definition(uri, request.dig(:params, :position))
172
+ when "rubyLsp/textDocument/showSyntaxTree"
173
+ { ast: Requests::ShowSyntaxTree.new(@store.get(uri)).run }
172
174
  end
173
175
  end
174
176
 
177
+ sig { params(uri: String, position: Document::PositionShape).returns(T.nilable(Interface::Location)) }
178
+ def definition(uri, position)
179
+ document = @store.get(uri)
180
+ return if document.syntax_error?
181
+
182
+ target, _parent = document.locate_node(position, node_types: [SyntaxTree::Command])
183
+
184
+ emitter = EventEmitter.new
185
+ base_listener = Requests::Definition.new(uri, emitter, @message_queue)
186
+ emitter.emit_for_target(target)
187
+ base_listener.response
188
+ end
189
+
175
190
  sig { params(uri: String).returns(T::Array[Interface::FoldingRange]) }
176
191
  def folding_range(uri)
177
192
  @store.cache_fetch(uri, "textDocument/foldingRange") do |document|
@@ -198,15 +213,13 @@ module RubyLsp
198
213
 
199
214
  # Instantiate all listeners
200
215
  emitter = EventEmitter.new
201
- base_listener = Requests::Hover.new(emitter, @message_queue)
202
- listeners = Requests::Hover.listeners.map { |l| l.new(emitter, @message_queue) }
216
+ hover = Requests::Hover.new(emitter, @message_queue)
203
217
 
204
218
  # Emit events for all listeners
205
219
  emitter.emit_for_target(target)
206
220
 
207
- # Merge all responses into a single hover
208
- listeners.each { |ext| base_listener.merge_response!(ext) }
209
- base_listener.response
221
+ hover.merge_external_listeners_responses!
222
+ hover.response
210
223
  end
211
224
 
212
225
  sig { params(uri: String, content_changes: T::Array[Document::EditShape], version: Integer).returns(Object) }
@@ -436,7 +449,9 @@ module RubyLsp
436
449
  end
437
450
 
438
451
  configured_features = options.dig(:initializationOptions, :enabledFeatures)
439
- experimental_features = options.dig(:initializationOptions, :experimentalFeaturesEnabled)
452
+
453
+ # Uncomment the line below and use the variable to gate features behind the experimental flag
454
+ # experimental_features = options.dig(:initializationOptions, :experimentalFeaturesEnabled)
440
455
 
441
456
  enabled_features = case configured_features
442
457
  when Array
@@ -465,7 +480,7 @@ module RubyLsp
465
480
  Interface::DocumentLinkOptions.new(resolve_provider: false)
466
481
  end
467
482
 
468
- code_lens_provider = if experimental_features
483
+ code_lens_provider = if enabled_features["codeLens"]
469
484
  Interface::CodeLensOptions.new(resolve_provider: false)
470
485
  end
471
486
 
@@ -539,6 +554,7 @@ module RubyLsp
539
554
  inlay_hint_provider: inlay_hint_provider,
540
555
  completion_provider: completion_provider,
541
556
  code_lens_provider: code_lens_provider,
557
+ definition_provider: enabled_features["definition"],
542
558
  ),
543
559
  )
544
560
  end
@@ -38,6 +38,8 @@ module RubyLsp
38
38
  def initialize(uri, emitter, message_queue, test_library)
39
39
  super(emitter, message_queue)
40
40
 
41
+ @uri = T.let(uri, String)
42
+ @external_listeners = T.let([], T::Array[RubyLsp::Listener[ResponseType]])
41
43
  @test_library = T.let(test_library, String)
42
44
  @response = T.let([], ResponseType)
43
45
  @path = T.let(T.must(URI(uri).path), String)
@@ -56,6 +58,22 @@ module RubyLsp
56
58
  :after_call,
57
59
  :on_vcall,
58
60
  )
61
+
62
+ register_external_listeners!
63
+ end
64
+
65
+ sig { void }
66
+ def register_external_listeners!
67
+ self.class.listeners.each do |l|
68
+ @external_listeners << T.unsafe(l).new(@uri, @emitter, @message_queue)
69
+ end
70
+ end
71
+
72
+ sig { void }
73
+ def merge_external_listeners_responses!
74
+ @external_listeners.each do |l|
75
+ merge_response!(l)
76
+ end
59
77
  end
60
78
 
61
79
  sig { params(node: SyntaxTree::ClassDeclaration).void }
@@ -0,0 +1,95 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Requests
6
+ # ![Definition demo](../../definition.gif)
7
+ #
8
+ # The [definition
9
+ # request](https://microsoft.github.io/language-server-protocol/specification#textDocument_definition) jumps to the
10
+ # definition of the symbol under the cursor.
11
+ #
12
+ # Currently, only jumping to required files is supported.
13
+ #
14
+ # # Example
15
+ #
16
+ # ```ruby
17
+ # require "some_gem/file" # <- Request go to definition on this string will take you to the file
18
+ # ```
19
+ class Definition < Listener
20
+ extend T::Sig
21
+ extend T::Generic
22
+
23
+ ResponseType = type_member { { fixed: T.nilable(Interface::Location) } }
24
+
25
+ sig { override.returns(ResponseType) }
26
+ attr_reader :response
27
+
28
+ sig { params(uri: String, emitter: EventEmitter, message_queue: Thread::Queue).void }
29
+ def initialize(uri, emitter, message_queue)
30
+ super(emitter, message_queue)
31
+
32
+ @uri = uri
33
+ @response = T.let(nil, ResponseType)
34
+ emitter.register(self, :on_command)
35
+ end
36
+
37
+ sig { params(node: SyntaxTree::Command).void }
38
+ def on_command(node)
39
+ message = node.message.value
40
+ return unless message == "require" || message == "require_relative"
41
+
42
+ argument = node.arguments.parts.first
43
+ return unless argument.is_a?(SyntaxTree::StringLiteral)
44
+
45
+ string = argument.parts.first
46
+ return unless string.is_a?(SyntaxTree::TStringContent)
47
+
48
+ required_file = "#{string.value}.rb"
49
+
50
+ case message
51
+ when "require"
52
+ candidate = find_file_in_load_path(required_file)
53
+
54
+ if candidate
55
+ @response = Interface::Location.new(
56
+ uri: "file://#{candidate}",
57
+ range: Interface::Range.new(
58
+ start: Interface::Position.new(line: 0, character: 0),
59
+ end: Interface::Position.new(line: 0, character: 0),
60
+ ),
61
+ )
62
+ end
63
+ when "require_relative"
64
+ current_file = T.must(URI.parse(@uri).path)
65
+ current_folder = Pathname.new(current_file).dirname
66
+ candidate = File.expand_path(File.join(current_folder, required_file))
67
+
68
+ if candidate
69
+ @response = Interface::Location.new(
70
+ uri: "file://#{candidate}",
71
+ range: Interface::Range.new(
72
+ start: Interface::Position.new(line: 0, character: 0),
73
+ end: Interface::Position.new(line: 0, character: 0),
74
+ ),
75
+ )
76
+ end
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ sig { params(file: String).returns(T.nilable(String)) }
83
+ def find_file_in_load_path(file)
84
+ return unless file.include?("/")
85
+
86
+ $LOAD_PATH.each do |p|
87
+ found = Dir.glob("**/#{file}", base: p).first
88
+ return "#{p}/#{found}" if found
89
+ end
90
+
91
+ nil
92
+ end
93
+ end
94
+ end
95
+ end
@@ -39,8 +39,25 @@ module RubyLsp
39
39
  def initialize(emitter, message_queue)
40
40
  super
41
41
 
42
+ @external_listeners = T.let([], T::Array[RubyLsp::Listener[ResponseType]])
42
43
  @response = T.let(nil, ResponseType)
43
44
  emitter.register(self, :on_command, :on_const_path_ref, :on_call)
45
+
46
+ register_external_listeners!
47
+ end
48
+
49
+ sig { void }
50
+ def register_external_listeners!
51
+ self.class.listeners.each do |l|
52
+ @external_listeners << T.unsafe(l).new(@emitter, @message_queue)
53
+ end
54
+ end
55
+
56
+ sig { void }
57
+ def merge_external_listeners_responses!
58
+ @external_listeners.each do |l|
59
+ merge_response!(l)
60
+ end
44
61
  end
45
62
 
46
63
  # Merges responses from other hover listeners
@@ -62,9 +62,34 @@ module RubyLsp
62
62
 
63
63
  sig { void }
64
64
  def handle_pipe
65
- return unless /((?<=do)|(?<={))\s+\|/.match?(@previous_line)
65
+ current_line = @lines[@position[:line]]
66
+ return unless /((?<=do)|(?<={))\s+\|/.match?(current_line)
67
+
68
+ line = T.must(current_line)
69
+
70
+ # If the current character is a pipe and both previous ones are pipes too, then we autocompleted a pipe and the
71
+ # user inserted a third one. In this case, we need to avoid adding a fourth and remove the previous one
72
+ if line[@position[:character] - 2] == "|" &&
73
+ line[@position[:character] - 1] == "|" &&
74
+ line[@position[:character]] == "|"
75
+
76
+ @edits << Interface::TextEdit.new(
77
+ range: Interface::Range.new(
78
+ start: Interface::Position.new(
79
+ line: @position[:line],
80
+ character: @position[:character],
81
+ ),
82
+ end: Interface::Position.new(
83
+ line: @position[:line],
84
+ character: @position[:character] + 1,
85
+ ),
86
+ ),
87
+ new_text: "",
88
+ )
89
+ else
90
+ add_edit_with_text("|")
91
+ end
66
92
 
67
- add_edit_with_text("|")
68
93
  move_cursor_to(@position[:line], @position[:character])
69
94
  end
70
95
 
@@ -88,10 +113,10 @@ module RubyLsp
88
113
  current_line = @lines[@position[:line]]
89
114
  next_line = @lines[@position[:line] + 1]
90
115
 
91
- if current_line.nil? || current_line.blank?
116
+ if current_line.nil? || current_line.strip.empty?
92
117
  add_edit_with_text(" \n#{indents}end")
93
118
  move_cursor_to(@position[:line], @indentation + 2)
94
- elsif next_line.nil? || next_line.blank?
119
+ elsif next_line.nil? || next_line.strip.empty?
95
120
  add_edit_with_text("#{indents}end", { line: @position[:line] + 1, character: @position[:character] })
96
121
  move_cursor_to(@position[:line], @indentation + 3)
97
122
  end
@@ -0,0 +1,33 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Requests
6
+ # ![Show syntax tree demo](../../show_syntax_tree.gif)
7
+ #
8
+ # Show syntax tree is a custom [LSP
9
+ # request](https://microsoft.github.io/language-server-protocol/specification#requestMessage) that displays the AST
10
+ # for the current document in a new tab.
11
+ #
12
+ # # Example
13
+ #
14
+ # ```ruby
15
+ # # Executing the Ruby LSP: Show syntax tree command will display the AST for the document
16
+ # 1 + 1
17
+ # # (program (statements ((binary (int "1") + (int "1")))))
18
+ # ```
19
+ #
20
+ class ShowSyntaxTree < BaseRequest
21
+ extend T::Sig
22
+
23
+ sig { override.returns(String) }
24
+ def run
25
+ return "Document contains syntax error" if @document.syntax_error?
26
+
27
+ output_string = +""
28
+ PP.pp(@document.tree, output_string)
29
+ output_string
30
+ end
31
+ end
32
+ end
33
+ end
@@ -19,6 +19,8 @@ module RubyLsp
19
19
  # - [InlayHint](rdoc-ref:RubyLsp::Requests::InlayHints)
20
20
  # - [PathCompletion](rdoc-ref:RubyLsp::Requests::PathCompletion)
21
21
  # - [CodeLens](rdoc-ref:RubyLsp::Requests::CodeLens)
22
+ # - [Definition](rdoc-ref:RubyLsp::Requests::Definition)
23
+ # - [ShowSyntaxTree](rdoc-ref:RubyLsp::Requests::ShowSyntaxTree)
22
24
 
23
25
  module Requests
24
26
  autoload :BaseRequest, "ruby_lsp/requests/base_request"
@@ -37,6 +39,8 @@ module RubyLsp
37
39
  autoload :InlayHints, "ruby_lsp/requests/inlay_hints"
38
40
  autoload :PathCompletion, "ruby_lsp/requests/path_completion"
39
41
  autoload :CodeLens, "ruby_lsp/requests/code_lens"
42
+ autoload :Definition, "ruby_lsp/requests/definition"
43
+ autoload :ShowSyntaxTree, "ruby_lsp/requests/show_syntax_tree"
40
44
 
41
45
  # :nodoc:
42
46
  module Support
@@ -70,6 +70,8 @@ module RubyLsp
70
70
  when "$/cancelRequest"
71
71
  # Cancel the job if it's still in the queue
72
72
  @mutex.synchronize { @jobs[request[:params][:id]]&.cancel }
73
+ when "$/setTrace"
74
+ VOID
73
75
  when "shutdown"
74
76
  warn("Shutting down Ruby LSP...")
75
77
 
@@ -0,0 +1,133 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler"
5
+ require "fileutils"
6
+
7
+ # This file is a script that will configure a custom bundle for the Ruby LSP. The custom bundle allows developers to use
8
+ # the Ruby LSP without including the gem in their application's Gemfile while at the same time giving us access to the
9
+ # exact locked versions of dependencies.
10
+
11
+ module RubyLsp
12
+ class SetupBundler
13
+ extend T::Sig
14
+
15
+ sig { params(project_path: String).void }
16
+ def initialize(project_path)
17
+ @project_path = project_path
18
+ @dependencies = T.let(load_dependencies, T::Hash[String, T.untyped])
19
+ end
20
+
21
+ sig { void }
22
+ def setup!
23
+ # Do not setup a custom bundle if we're working on the Ruby LSP, since it's already included by default
24
+ if File.basename(@project_path) == "ruby-lsp"
25
+ warn("Ruby LSP> Skipping custom bundle setup since we're working on the Ruby LSP itself")
26
+ run_bundle_install
27
+ return
28
+ end
29
+
30
+ # Do not setup a custom bundle if both `ruby-lsp` and `debug` are already in the Gemfile
31
+ if @dependencies["ruby-lsp"] && @dependencies["debug"]
32
+ warn("Ruby LSP> Skipping custom bundle setup since both `ruby-lsp` and `debug` are already in the Gemfile")
33
+ run_bundle_install
34
+ return
35
+ end
36
+
37
+ # Automatically create and ignore the .ruby-lsp folder for users
38
+ FileUtils.mkdir(".ruby-lsp") unless Dir.exist?(".ruby-lsp")
39
+ File.write(".ruby-lsp/.gitignore", "*") unless File.exist?(".ruby-lsp/.gitignore")
40
+
41
+ # Write the custom `.ruby-lsp/Gemfile` if it doesn't exist or if the content doesn't match
42
+ content = custom_gemfile_content
43
+
44
+ unless File.exist?(".ruby-lsp/Gemfile") && File.read(".ruby-lsp/Gemfile") == content
45
+ File.write(".ruby-lsp/Gemfile", content)
46
+ end
47
+
48
+ # If .ruby-lsp/Gemfile.lock already exists and the top level Gemfile.lock hasn't been modified since it was last
49
+ # updated, then we're ready to boot the server
50
+ if File.exist?(".ruby-lsp/Gemfile.lock") &&
51
+ File.stat(".ruby-lsp/Gemfile.lock").mtime > File.stat("Gemfile.lock").mtime
52
+ warn("Ruby LSP> Skipping custom bundle setup since .ruby-lsp/Gemfile.lock already exists and is up to date")
53
+ run_bundle_install(".ruby-lsp/Gemfile")
54
+ return
55
+ end
56
+
57
+ FileUtils.cp("Gemfile.lock", ".ruby-lsp/Gemfile.lock")
58
+ run_bundle_install(".ruby-lsp/Gemfile")
59
+ end
60
+
61
+ private
62
+
63
+ sig { returns(String) }
64
+ def custom_gemfile_content
65
+ parts = [
66
+ "# This custom gemfile is automatically generated by the Ruby LSP.",
67
+ "# It should be automatically git ignored, but in any case: do not commit it to your repository.",
68
+ "",
69
+ "eval_gemfile(File.expand_path(\"../Gemfile\", __dir__))",
70
+ ]
71
+
72
+ unless @dependencies["ruby-lsp"]
73
+ parts << 'gem "ruby-lsp", require: false, group: :development, source: "https://rubygems.org"'
74
+ end
75
+
76
+ unless @dependencies["debug"]
77
+ parts << 'gem "debug", require: false, group: :development, platforms: :mri, source: "https://rubygems.org"'
78
+ end
79
+
80
+ parts.join("\n")
81
+ end
82
+
83
+ sig { returns(T::Hash[String, T.untyped]) }
84
+ def load_dependencies
85
+ # We need to parse the Gemfile.lock manually here. If we try to do `bundler/setup` to use something more
86
+ # convenient, we may end up with issues when the globally installed `ruby-lsp` version mismatches the one included
87
+ # in the `Gemfile`
88
+ dependencies = Bundler::LockfileParser.new(Bundler.read_file("Gemfile.lock")).dependencies
89
+
90
+ # When working on a gem, the `ruby-lsp` might be listed as a dependency in the gemspec. We need to make sure we
91
+ # check those as well or else we may get version mismatch errors
92
+ gemspec_path = Dir.glob("*.gemspec").first
93
+ if gemspec_path
94
+ gemspec_dependencies = Bundler.load_gemspec(gemspec_path).dependencies.to_h { |dep| [dep.name, dep] }
95
+ dependencies.merge!(gemspec_dependencies)
96
+ end
97
+
98
+ dependencies
99
+ end
100
+
101
+ sig { params(bundle_gemfile: T.untyped).void }
102
+ def run_bundle_install(bundle_gemfile = nil)
103
+ # If the user has a custom bundle path configured, we need to ensure that we will use the absolute and not
104
+ # relative version of it when running `bundle install`. This is necessary to avoid installing the gems under the
105
+ # `.ruby-lsp` folder, which is not the user's intention. For example, if the path is configured as `vendor`, we
106
+ # want to install it in the top level `vendor` and not `.ruby-lsp/vendor`
107
+ path = Bundler.settings["path"]
108
+
109
+ command = +""
110
+ # Use the absolute `BUNDLE_PATH` to prevent accidentally creating unwanted folders under `.ruby-lsp`
111
+ command << "BUNDLE_PATH=#{File.expand_path(path, Dir.pwd)} " if path
112
+ command << "BUNDLE_GEMFILE=#{bundle_gemfile} " if bundle_gemfile
113
+
114
+ if @dependencies["ruby-lsp"] && @dependencies["debug"]
115
+ # Install gems using the custom bundle
116
+ command << "bundle install "
117
+ else
118
+ # If ruby-lsp or debug are not in the Gemfile, try to update them to the latest version
119
+ command << "bundle update "
120
+ command << "ruby-lsp " unless @dependencies["ruby-lsp"]
121
+ command << "debug " unless @dependencies["debug"]
122
+ end
123
+
124
+ # Redirect stdout to stderr to prevent going into an infinite loop. The extension might confuse stdout output with
125
+ # responses
126
+ command << "1>&2"
127
+
128
+ # Add bundle update
129
+ warn("Ruby LSP> Running bundle install for the custom bundle. This may take a while...")
130
+ system(command)
131
+ end
132
+ end
133
+ end
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.6.2
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-06-23 00:00:00.000000000 Z
11
+ date: 2023-07-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: language_server-protocol
@@ -63,6 +63,7 @@ email:
63
63
  - ruby@shopify.com
64
64
  executables:
65
65
  - ruby-lsp
66
+ - ruby-lsp-check
66
67
  extensions: []
67
68
  extra_rdoc_files: []
68
69
  files:
@@ -70,6 +71,7 @@ files:
70
71
  - README.md
71
72
  - VERSION
72
73
  - exe/ruby-lsp
74
+ - exe/ruby-lsp-check
73
75
  - lib/rubocop/cop/ruby_lsp/use_language_server_aliases.rb
74
76
  - lib/ruby-lsp.rb
75
77
  - lib/ruby_lsp/check_docs.rb
@@ -84,6 +86,7 @@ files:
84
86
  - lib/ruby_lsp/requests/code_action_resolve.rb
85
87
  - lib/ruby_lsp/requests/code_actions.rb
86
88
  - lib/ruby_lsp/requests/code_lens.rb
89
+ - lib/ruby_lsp/requests/definition.rb
87
90
  - lib/ruby_lsp/requests/diagnostics.rb
88
91
  - lib/ruby_lsp/requests/document_highlight.rb
89
92
  - lib/ruby_lsp/requests/document_link.rb
@@ -96,6 +99,7 @@ files:
96
99
  - lib/ruby_lsp/requests/path_completion.rb
97
100
  - lib/ruby_lsp/requests/selection_ranges.rb
98
101
  - lib/ruby_lsp/requests/semantic_highlighting.rb
102
+ - lib/ruby_lsp/requests/show_syntax_tree.rb
99
103
  - lib/ruby_lsp/requests/support/annotation.rb
100
104
  - lib/ruby_lsp/requests/support/common.rb
101
105
  - lib/ruby_lsp/requests/support/dependency_detector.rb
@@ -113,6 +117,7 @@ files:
113
117
  - lib/ruby_lsp/requests/support/source_uri.rb
114
118
  - lib/ruby_lsp/requests/support/syntax_tree_formatting_runner.rb
115
119
  - lib/ruby_lsp/server.rb
120
+ - lib/ruby_lsp/setup_bundler.rb
116
121
  - lib/ruby_lsp/store.rb
117
122
  - lib/ruby_lsp/utils.rb
118
123
  homepage: https://github.com/Shopify/ruby-lsp
@@ -135,7 +140,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
135
140
  - !ruby/object:Gem::Version
136
141
  version: '0'
137
142
  requirements: []
138
- rubygems_version: 3.4.14
143
+ rubygems_version: 3.4.16
139
144
  signing_key:
140
145
  specification_version: 4
141
146
  summary: An opinionated language server for Ruby