ruby-lsp 0.6.2 → 0.7.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: 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