ruby-lsp 0.8.0 → 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,35 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "test_helper"
5
+
6
+ module RubyIndexer
7
+ class ConfigurationTest < Minitest::Test
8
+ def setup
9
+ @config = Configuration.new
10
+ end
11
+
12
+ def test_load_configuration_executes_configure_block
13
+ @config.load_config
14
+ files_to_index = @config.files_to_index
15
+
16
+ assert(files_to_index.none? { |path| path.include?("test/fixtures") })
17
+ assert(files_to_index.none? { |path| path.include?("minitest-reporters") })
18
+ end
19
+
20
+ def test_paths_are_unique
21
+ @config.load_config
22
+ files_to_index = @config.files_to_index
23
+
24
+ assert_equal(files_to_index.uniq.length, files_to_index.length)
25
+ end
26
+
27
+ def test_configuration_raises_for_unknown_keys
28
+ Psych::Nodes::Document.any_instance.expects(:to_ruby).returns({ "unknown_config" => 123 })
29
+
30
+ assert_raises(ArgumentError) do
31
+ @config.load_config
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,108 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "test_case"
5
+
6
+ module RubyIndexer
7
+ class ConstantTest < TestCase
8
+ def test_constant_writes
9
+ index(<<~RUBY)
10
+ FOO = 1
11
+
12
+ class ::Bar
13
+ FOO = 2
14
+ end
15
+ RUBY
16
+
17
+ assert_entry("FOO", Index::Entry::Constant, "/fake/path/foo.rb:0-0:0-6")
18
+ assert_entry("Bar::FOO", Index::Entry::Constant, "/fake/path/foo.rb:3-2:3-8")
19
+ end
20
+
21
+ def test_constant_or_writes
22
+ index(<<~RUBY)
23
+ FOO ||= 1
24
+
25
+ class ::Bar
26
+ FOO ||= 2
27
+ end
28
+ RUBY
29
+
30
+ assert_entry("FOO", Index::Entry::Constant, "/fake/path/foo.rb:0-0:0-8")
31
+ assert_entry("Bar::FOO", Index::Entry::Constant, "/fake/path/foo.rb:3-2:3-10")
32
+ end
33
+
34
+ def test_constant_path_writes
35
+ index(<<~RUBY)
36
+ class A
37
+ FOO = 1
38
+ ::BAR = 1
39
+
40
+ module B
41
+ FOO = 1
42
+ end
43
+ end
44
+
45
+ A::BAZ = 1
46
+ RUBY
47
+
48
+ assert_entry("A::FOO", Index::Entry::Constant, "/fake/path/foo.rb:1-2:1-8")
49
+ assert_entry("BAR", Index::Entry::Constant, "/fake/path/foo.rb:2-2:2-10")
50
+ assert_entry("A::B::FOO", Index::Entry::Constant, "/fake/path/foo.rb:5-4:5-10")
51
+ assert_entry("A::BAZ", Index::Entry::Constant, "/fake/path/foo.rb:9-0:9-9")
52
+ end
53
+
54
+ def test_constant_path_or_writes
55
+ index(<<~RUBY)
56
+ class A
57
+ FOO ||= 1
58
+ ::BAR ||= 1
59
+ end
60
+
61
+ A::BAZ ||= 1
62
+ RUBY
63
+
64
+ assert_entry("A::FOO", Index::Entry::Constant, "/fake/path/foo.rb:1-2:1-10")
65
+ assert_entry("BAR", Index::Entry::Constant, "/fake/path/foo.rb:2-2:2-12")
66
+ assert_entry("A::BAZ", Index::Entry::Constant, "/fake/path/foo.rb:5-0:5-11")
67
+ end
68
+
69
+ def test_comments_for_constants
70
+ index(<<~RUBY)
71
+ # FOO comment
72
+ FOO = 1
73
+
74
+ class A
75
+ # A::FOO comment
76
+ FOO = 1
77
+
78
+ # ::BAR comment
79
+ ::BAR = 1
80
+ end
81
+
82
+ # A::BAZ comment
83
+ A::BAZ = 1
84
+ RUBY
85
+
86
+ foo_comment = @index["FOO"].first.comments.join("\n")
87
+ assert_equal("# FOO comment\n", foo_comment)
88
+
89
+ a_foo_comment = @index["A::FOO"].first.comments.join("\n")
90
+ assert_equal("# A::FOO comment\n", a_foo_comment)
91
+
92
+ bar_comment = @index["BAR"].first.comments.join("\n")
93
+ assert_equal("# ::BAR comment\n", bar_comment)
94
+
95
+ a_baz_comment = @index["A::BAZ"].first.comments.join("\n")
96
+ assert_equal("# A::BAZ comment\n", a_baz_comment)
97
+ end
98
+
99
+ def test_variable_path_constants_are_ignored
100
+ index(<<~RUBY)
101
+ var::FOO = 1
102
+ self.class::FOO = 1
103
+ RUBY
104
+
105
+ assert_no_entry
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,94 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "test_case"
5
+
6
+ module RubyIndexer
7
+ class IndexTest < TestCase
8
+ def test_deleting_one_entry_for_a_class
9
+ @index.index_single("/fake/path/foo.rb", <<~RUBY)
10
+ class Foo
11
+ end
12
+ RUBY
13
+ @index.index_single("/fake/path/other_foo.rb", <<~RUBY)
14
+ class Foo
15
+ end
16
+ RUBY
17
+
18
+ entries = @index["Foo"]
19
+ assert_equal(2, entries.length)
20
+
21
+ @index.delete("/fake/path/other_foo.rb")
22
+ entries = @index["Foo"]
23
+ assert_equal(1, entries.length)
24
+ end
25
+
26
+ def test_deleting_all_entries_for_a_class
27
+ @index.index_single("/fake/path/foo.rb", <<~RUBY)
28
+ class Foo
29
+ end
30
+ RUBY
31
+
32
+ entries = @index["Foo"]
33
+ assert_equal(1, entries.length)
34
+
35
+ @index.delete("/fake/path/foo.rb")
36
+ entries = @index["Foo"]
37
+ assert_nil(entries)
38
+ end
39
+
40
+ def test_index_resolve
41
+ @index.index_single("/fake/path/foo.rb", <<~RUBY)
42
+ class Bar; end
43
+
44
+ module Foo
45
+ class Bar
46
+ end
47
+
48
+ class Baz
49
+ class Something
50
+ end
51
+ end
52
+ end
53
+ RUBY
54
+
55
+ entries = @index.resolve("Something", ["Foo", "Baz"])
56
+ refute_empty(entries)
57
+ assert_equal("Foo::Baz::Something", entries.first.name)
58
+
59
+ entries = @index.resolve("Bar", ["Foo"])
60
+ refute_empty(entries)
61
+ assert_equal("Foo::Bar", entries.first.name)
62
+
63
+ entries = @index.resolve("Bar", ["Foo", "Baz"])
64
+ refute_empty(entries)
65
+ assert_equal("Foo::Bar", entries.first.name)
66
+
67
+ entries = @index.resolve("Foo::Bar", ["Foo", "Baz"])
68
+ refute_empty(entries)
69
+ assert_equal("Foo::Bar", entries.first.name)
70
+
71
+ assert_nil(@index.resolve("DoesNotExist", ["Foo"]))
72
+ end
73
+
74
+ def test_accessing_with_colon_colon_prefix
75
+ @index.index_single("/fake/path/foo.rb", <<~RUBY)
76
+ class Bar; end
77
+
78
+ module Foo
79
+ class Bar
80
+ end
81
+
82
+ class Baz
83
+ class Something
84
+ end
85
+ end
86
+ end
87
+ RUBY
88
+
89
+ entries = @index["::Foo::Baz::Something"]
90
+ refute_empty(entries)
91
+ assert_equal("Foo::Baz::Something", entries.first.name)
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,42 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "test_helper"
5
+
6
+ module RubyIndexer
7
+ class TestCase < Minitest::Test
8
+ def setup
9
+ @index = Index.new
10
+ end
11
+
12
+ private
13
+
14
+ def index(source)
15
+ @index.index_single("/fake/path/foo.rb", source)
16
+ end
17
+
18
+ def assert_entry(expected_name, type, expected_location)
19
+ entries = @index[expected_name]
20
+ refute_empty(entries, "Expected #{expected_name} to be indexed")
21
+
22
+ entry = entries.first
23
+ assert_instance_of(type, entry, "Expected #{expected_name} to be a #{type}")
24
+
25
+ location = entry.location
26
+ location_string =
27
+ "#{entry.file_path}:#{location.start_line - 1}-#{location.start_column}" \
28
+ ":#{location.end_line - 1}-#{location.end_column}"
29
+
30
+ assert_equal(expected_location, location_string)
31
+ end
32
+
33
+ def refute_entry(expected_name)
34
+ entries = @index[expected_name]
35
+ assert_nil(entries, "Expected #{expected_name} to not be indexed")
36
+ end
37
+
38
+ def assert_no_entry
39
+ assert_empty(@index.instance_variable_get(:@entries), "Expected nothing to be indexed")
40
+ end
41
+ end
42
+ end
@@ -15,6 +15,7 @@ module RubyLsp
15
15
  @store = store
16
16
  @test_library = T.let(DependencyDetector.detected_test_library, String)
17
17
  @message_queue = message_queue
18
+ @index = T.let(RubyIndexer::Index.new, RubyIndexer::Index)
18
19
  end
19
20
 
20
21
  sig { params(request: T::Hash[Symbol, T.untyped]).returns(Result) }
@@ -57,6 +58,14 @@ module RubyLsp
57
58
  warn(errored_extensions.map(&:backtraces).join("\n\n"))
58
59
  end
59
60
 
61
+ if @store.experimental_features
62
+ # The begin progress invocation happens during `initialize`, so that the notification is sent before we are
63
+ # stuck indexing files
64
+ RubyIndexer.configuration.load_config
65
+ @index.index_all
66
+ end_progress("indexing-progress")
67
+ end
68
+
60
69
  check_formatter_is_available
61
70
 
62
71
  warn("Ruby LSP is ready")
@@ -169,25 +178,59 @@ module RubyLsp
169
178
  completion(uri, request.dig(:params, :position))
170
179
  when "textDocument/definition"
171
180
  definition(uri, request.dig(:params, :position))
181
+ when "workspace/didChangeWatchedFiles"
182
+ did_change_watched_files(request.dig(:params, :changes))
172
183
  when "rubyLsp/textDocument/showSyntaxTree"
173
184
  show_syntax_tree(uri, request.dig(:params, :range))
174
185
  end
175
186
  end
176
187
 
188
+ sig { params(changes: T::Array[{ uri: String, type: Integer }]).returns(Object) }
189
+ def did_change_watched_files(changes)
190
+ changes.each do |change|
191
+ # File change events include folders, but we're only interested in files
192
+ uri = URI(change[:uri])
193
+ file_path = uri.to_standardized_path
194
+ next if file_path.nil? || File.directory?(file_path)
195
+
196
+ case change[:type]
197
+ when Constant::FileChangeType::CREATED
198
+ @index.index_single(file_path)
199
+ when Constant::FileChangeType::CHANGED
200
+ @index.delete(file_path)
201
+ @index.index_single(file_path)
202
+ when Constant::FileChangeType::DELETED
203
+ @index.delete(file_path)
204
+ end
205
+ end
206
+
207
+ VOID
208
+ end
209
+
177
210
  sig { params(uri: URI::Generic, range: T.nilable(Document::RangeShape)).returns({ ast: String }) }
178
211
  def show_syntax_tree(uri, range)
179
212
  { ast: Requests::ShowSyntaxTree.new(@store.get(uri), range).run }
180
213
  end
181
214
 
182
- sig { params(uri: URI::Generic, position: Document::PositionShape).returns(T.nilable(Interface::Location)) }
215
+ sig do
216
+ params(
217
+ uri: URI::Generic,
218
+ position: Document::PositionShape,
219
+ ).returns(T.nilable(T.any(T::Array[Interface::Location], Interface::Location)))
220
+ end
183
221
  def definition(uri, position)
184
222
  document = @store.get(uri)
185
223
  return if document.syntax_error?
186
224
 
187
- target, _parent = document.locate_node(position, node_types: [SyntaxTree::Command])
225
+ target, parent, nesting = document.locate_node(
226
+ position,
227
+ node_types: [SyntaxTree::Command, SyntaxTree::Const, SyntaxTree::ConstPathRef],
228
+ )
229
+
230
+ target = parent if target.is_a?(SyntaxTree::Const) && parent.is_a?(SyntaxTree::ConstPathRef)
188
231
 
189
232
  emitter = EventEmitter.new
190
- base_listener = Requests::Definition.new(uri, emitter, @message_queue)
233
+ base_listener = Requests::Definition.new(uri, nesting, @index, emitter, @message_queue)
191
234
  emitter.emit_for_target(target)
192
235
  base_listener.response
193
236
  end
@@ -436,6 +479,37 @@ module RubyLsp
436
479
  listener.response
437
480
  end
438
481
 
482
+ sig { params(id: String, title: String).void }
483
+ def begin_progress(id, title)
484
+ return unless @store.supports_progress
485
+
486
+ @message_queue << Request.new(
487
+ message: "window/workDoneProgress/create",
488
+ params: Interface::WorkDoneProgressCreateParams.new(token: id),
489
+ )
490
+
491
+ @message_queue << Notification.new(
492
+ message: "$/progress",
493
+ params: Interface::ProgressParams.new(
494
+ token: id,
495
+ value: Interface::WorkDoneProgressBegin.new(kind: "begin", title: title),
496
+ ),
497
+ )
498
+ end
499
+
500
+ sig { params(id: String).void }
501
+ def end_progress(id)
502
+ return unless @store.supports_progress
503
+
504
+ @message_queue << Notification.new(
505
+ message: "$/progress",
506
+ params: Interface::ProgressParams.new(
507
+ token: id,
508
+ value: Interface::WorkDoneProgressEnd.new(kind: "end"),
509
+ ),
510
+ )
511
+ end
512
+
439
513
  sig { params(options: T::Hash[Symbol, T.untyped]).returns(Interface::InitializeResult) }
440
514
  def initialize_request(options)
441
515
  @store.clear
@@ -449,6 +523,7 @@ module RubyLsp
449
523
  encodings.first
450
524
  end
451
525
 
526
+ @store.supports_progress = options.dig(:capabilities, :window, :workDoneProgress) || true
452
527
  formatter = options.dig(:initializationOptions, :formatter) || "auto"
453
528
  @store.formatter = if formatter == "auto"
454
529
  DependencyDetector.detected_formatter
@@ -457,9 +532,7 @@ module RubyLsp
457
532
  end
458
533
 
459
534
  configured_features = options.dig(:initializationOptions, :enabledFeatures)
460
-
461
- # Uncomment the line below and use the variable to gate features behind the experimental flag
462
- # experimental_features = options.dig(:initializationOptions, :experimentalFeaturesEnabled)
535
+ @store.experimental_features = options.dig(:initializationOptions, :experimentalFeaturesEnabled) || false
463
536
 
464
537
  enabled_features = case configured_features
465
538
  when Array
@@ -541,6 +614,37 @@ module RubyLsp
541
614
  )
542
615
  end
543
616
 
617
+ if @store.experimental_features
618
+ # Dynamically registered capabilities
619
+ file_watching_caps = options.dig(:capabilities, :workspace, :didChangeWatchedFiles)
620
+
621
+ # Not every client supports dynamic registration or file watching
622
+ if file_watching_caps&.dig(:dynamicRegistration) && file_watching_caps&.dig(:relativePatternSupport)
623
+ @message_queue << Request.new(
624
+ message: "client/registerCapability",
625
+ params: Interface::RegistrationParams.new(
626
+ registrations: [
627
+ # Register watching Ruby files
628
+ Interface::Registration.new(
629
+ id: "workspace/didChangeWatchedFiles",
630
+ method: "workspace/didChangeWatchedFiles",
631
+ register_options: Interface::DidChangeWatchedFilesRegistrationOptions.new(
632
+ watchers: [
633
+ Interface::FileSystemWatcher.new(
634
+ glob_pattern: "**/*.rb",
635
+ kind: Constant::WatchKind::CREATE | Constant::WatchKind::CHANGE | Constant::WatchKind::DELETE,
636
+ ),
637
+ ],
638
+ ),
639
+ ),
640
+ ],
641
+ ),
642
+ )
643
+ end
644
+
645
+ begin_progress("indexing-progress", "Ruby LSP: indexing files")
646
+ end
647
+
544
648
  Interface::InitializeResult.new(
545
649
  capabilities: Interface::ServerCapabilities.new(
546
650
  text_document_sync: Interface::TextDocumentSyncOptions.new(
@@ -3,6 +3,7 @@
3
3
 
4
4
  require "sorbet-runtime"
5
5
  require "syntax_tree"
6
+ require "yarp"
6
7
  require "language_server-protocol"
7
8
  require "benchmark"
8
9
  require "bundler"
@@ -10,6 +11,7 @@ require "uri"
10
11
  require "cgi"
11
12
 
12
13
  require "ruby-lsp"
14
+ require "ruby_indexer/ruby_indexer"
13
15
  require "core_ext/uri"
14
16
  require "ruby_lsp/utils"
15
17
  require "ruby_lsp/server"
@@ -20,18 +20,41 @@ module RubyLsp
20
20
  extend T::Sig
21
21
  extend T::Generic
22
22
 
23
- ResponseType = type_member { { fixed: T.nilable(Interface::Location) } }
23
+ ResponseType = type_member { { fixed: T.nilable(T.any(T::Array[Interface::Location], Interface::Location)) } }
24
24
 
25
25
  sig { override.returns(ResponseType) }
26
26
  attr_reader :response
27
27
 
28
- sig { params(uri: URI::Generic, emitter: EventEmitter, message_queue: Thread::Queue).void }
29
- def initialize(uri, emitter, message_queue)
28
+ HAS_TYPECHECKER = T.let(DependencyDetector.typechecker?, T::Boolean)
29
+
30
+ sig do
31
+ params(
32
+ uri: URI::Generic,
33
+ nesting: T::Array[String],
34
+ index: RubyIndexer::Index,
35
+ emitter: EventEmitter,
36
+ message_queue: Thread::Queue,
37
+ ).void
38
+ end
39
+ def initialize(uri, nesting, index, emitter, message_queue)
30
40
  super(emitter, message_queue)
31
41
 
32
42
  @uri = uri
43
+ @nesting = nesting
44
+ @index = index
33
45
  @response = T.let(nil, ResponseType)
34
- emitter.register(self, :on_command)
46
+ emitter.register(self, :on_command, :on_const, :on_const_path_ref)
47
+ end
48
+
49
+ sig { params(node: SyntaxTree::ConstPathRef).void }
50
+ def on_const_path_ref(node)
51
+ name = full_constant_name(node)
52
+ find_in_index(name)
53
+ end
54
+
55
+ sig { params(node: SyntaxTree::Const).void }
56
+ def on_const(node)
57
+ find_in_index(node.value)
35
58
  end
36
59
 
37
60
  sig { params(node: SyntaxTree::Command).void }
@@ -79,6 +102,30 @@ module RubyLsp
79
102
 
80
103
  private
81
104
 
105
+ sig { params(value: String).void }
106
+ def find_in_index(value)
107
+ entries = @index.resolve(value, @nesting)
108
+ return unless entries
109
+
110
+ workspace_path = T.must(WORKSPACE_URI.to_standardized_path)
111
+
112
+ @response = entries.filter_map do |entry|
113
+ location = entry.location
114
+ # If the project has Sorbet, then we only want to handle go to definition for constants defined in gems, as an
115
+ # additional behavior on top of jumping to RBIs. Sorbet can already handle go to definition for all constants
116
+ # in the project, even if the files are typed false
117
+ next if HAS_TYPECHECKER && entry.file_path.start_with?(workspace_path)
118
+
119
+ Interface::Location.new(
120
+ uri: URI::Generic.from_path(path: entry.file_path).to_s,
121
+ range: Interface::Range.new(
122
+ start: Interface::Position.new(line: location.start_line - 1, character: location.start_column),
123
+ end: Interface::Position.new(line: location.end_line - 1, character: location.end_column),
124
+ ),
125
+ )
126
+ end
127
+ end
128
+
82
129
  sig { params(file: String).returns(T.nilable(String)) }
83
130
  def find_file_in_load_path(file)
84
131
  return unless file.include?("/")
@@ -67,12 +67,10 @@ module RubyLsp
67
67
 
68
68
  line = T.must(current_line)
69
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
-
70
+ # If the user inserts the closing pipe manually to the end of the block argument, we need to avoid adding
71
+ # an additional one and remove the previous one. This also helps to remove the user who accidentally
72
+ # inserts another pipe after the autocompleted one.
73
+ if line[..@position[:character]] =~ /(do|{)\s+\|[^|]*\|\|$/
76
74
  @edits << Interface::TextEdit.new(
77
75
  range: Interface::Range.new(
78
76
  start: Interface::Position.new(
@@ -36,6 +36,11 @@ module RubyLsp
36
36
  end
37
37
  end
38
38
 
39
+ sig { returns(T::Boolean) }
40
+ def typechecker?
41
+ direct_dependency?(/^sorbet/) || direct_dependency?(/^sorbet-static-and-runtime/)
42
+ end
43
+
39
44
  sig { params(gem_pattern: Regexp).returns(T::Boolean) }
40
45
  def direct_dependency?(gem_pattern)
41
46
  Bundler.with_original_env { Bundler.default_gemfile } &&
@@ -17,9 +17,10 @@ module RubyLsp
17
17
 
18
18
  class BundleNotLocked < StandardError; end
19
19
 
20
- sig { params(project_path: String).void }
21
- def initialize(project_path)
20
+ sig { params(project_path: String, branch: T.nilable(String)).void }
21
+ def initialize(project_path, branch: nil)
22
22
  @project_path = project_path
23
+ @branch = branch
23
24
 
24
25
  # Custom bundle paths
25
26
  @custom_dir = T.let(Pathname.new(".ruby-lsp").expand_path(Dir.pwd), Pathname)
@@ -119,7 +120,9 @@ module RubyLsp
119
120
  end
120
121
 
121
122
  unless @dependencies["ruby-lsp"]
122
- parts << 'gem "ruby-lsp", require: false, group: :development'
123
+ ruby_lsp_entry = +'gem "ruby-lsp", require: false, group: :development'
124
+ ruby_lsp_entry << ", github: \"Shopify/ruby-lsp\", branch: \"#{@branch}\"" if @branch
125
+ parts << ruby_lsp_entry
123
126
  end
124
127
 
125
128
  unless @dependencies["debug"]
@@ -156,11 +159,12 @@ module RubyLsp
156
159
  # `.ruby-lsp` folder, which is not the user's intention. For example, if the path is configured as `vendor`, we
157
160
  # want to install it in the top level `vendor` and not `.ruby-lsp/vendor`
158
161
  path = Bundler.settings["path"]
162
+ expanded_path = File.expand_path(path, Dir.pwd) if path
159
163
 
160
164
  # Use the absolute `BUNDLE_PATH` to prevent accidentally creating unwanted folders under `.ruby-lsp`
161
165
  env = {}
162
166
  env["BUNDLE_GEMFILE"] = bundle_gemfile.to_s
163
- env["BUNDLE_PATH"] = File.expand_path(path, Dir.pwd) if path
167
+ env["BUNDLE_PATH"] = expanded_path if expanded_path
164
168
 
165
169
  # If both `ruby-lsp` and `debug` are already in the Gemfile, then we shouldn't try to upgrade them or else we'll
166
170
  # produce undesired source control changes. If the custom bundle was just created and either `ruby-lsp` or `debug`
@@ -187,7 +191,7 @@ module RubyLsp
187
191
  # Add bundle update
188
192
  warn("Ruby LSP> Running bundle install for the custom bundle. This may take a while...")
189
193
  system(env, command)
190
- [bundle_gemfile.to_s, path]
194
+ [bundle_gemfile.to_s, expanded_path]
191
195
  end
192
196
  end
193
197
  end
@@ -13,34 +13,40 @@ module RubyLsp
13
13
  sig { returns(String) }
14
14
  attr_accessor :formatter
15
15
 
16
+ sig { returns(T::Boolean) }
17
+ attr_accessor :supports_progress
18
+
19
+ sig { returns(T::Boolean) }
20
+ attr_accessor :experimental_features
21
+
16
22
  sig { void }
17
23
  def initialize
18
24
  @state = T.let({}, T::Hash[String, Document])
19
25
  @encoding = T.let(Constant::PositionEncodingKind::UTF8, String)
20
26
  @formatter = T.let("auto", String)
27
+ @supports_progress = T.let(true, T::Boolean)
28
+ @experimental_features = T.let(false, T::Boolean)
21
29
  end
22
30
 
23
31
  sig { params(uri: URI::Generic).returns(Document) }
24
32
  def get(uri)
25
- path = uri.to_standardized_path
26
- return T.must(@state[T.must(uri.opaque)]) unless path
27
-
28
- document = @state[path]
33
+ document = @state[uri.to_s]
29
34
  return document unless document.nil?
30
35
 
31
- set(uri: uri, source: File.binread(CGI.unescape(path)), version: 0)
32
- T.must(@state[path])
36
+ path = T.must(uri.to_standardized_path)
37
+ set(uri: uri, source: File.binread(path), version: 0)
38
+ T.must(@state[uri.to_s])
33
39
  end
34
40
 
35
41
  sig { params(uri: URI::Generic, source: String, version: Integer).void }
36
42
  def set(uri:, source:, version:)
37
43
  document = Document.new(source: source, version: version, uri: uri, encoding: @encoding)
38
- @state[uri.storage_key] = document
44
+ @state[uri.to_s] = document
39
45
  end
40
46
 
41
47
  sig { params(uri: URI::Generic, edits: T::Array[Document::EditShape], version: Integer).void }
42
48
  def push_edits(uri:, edits:, version:)
43
- T.must(@state[uri.storage_key]).push_edits(edits, version: version)
49
+ T.must(@state[uri.to_s]).push_edits(edits, version: version)
44
50
  end
45
51
 
46
52
  sig { void }
@@ -55,7 +61,7 @@ module RubyLsp
55
61
 
56
62
  sig { params(uri: URI::Generic).void }
57
63
  def delete(uri)
58
- @state.delete(uri.storage_key)
64
+ @state.delete(uri.to_s)
59
65
  end
60
66
 
61
67
  sig do