ruby-lsp 0.7.6 → 0.8.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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/exe/ruby-lsp +41 -33
  4. data/exe/ruby-lsp-check +2 -2
  5. data/lib/core_ext/uri.rb +40 -0
  6. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +91 -0
  7. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +122 -0
  8. data/lib/ruby_indexer/lib/ruby_indexer/visitor.rb +121 -0
  9. data/lib/ruby_indexer/ruby_indexer.rb +19 -0
  10. data/lib/ruby_indexer/test/classes_and_modules_test.rb +204 -0
  11. data/lib/ruby_indexer/test/configuration_test.rb +35 -0
  12. data/lib/ruby_indexer/test/constant_test.rb +108 -0
  13. data/lib/ruby_indexer/test/index_test.rb +94 -0
  14. data/lib/ruby_indexer/test/test_case.rb +42 -0
  15. data/lib/ruby_lsp/document.rb +3 -3
  16. data/lib/ruby_lsp/executor.rb +131 -24
  17. data/lib/ruby_lsp/extension.rb +24 -0
  18. data/lib/ruby_lsp/internal.rb +4 -0
  19. data/lib/ruby_lsp/listener.rb +15 -14
  20. data/lib/ruby_lsp/requests/code_actions.rb +3 -3
  21. data/lib/ruby_lsp/requests/code_lens.rb +10 -24
  22. data/lib/ruby_lsp/requests/definition.rb +55 -8
  23. data/lib/ruby_lsp/requests/diagnostics.rb +3 -2
  24. data/lib/ruby_lsp/requests/document_link.rb +4 -3
  25. data/lib/ruby_lsp/requests/formatting.rb +3 -2
  26. data/lib/ruby_lsp/requests/hover.rb +4 -18
  27. data/lib/ruby_lsp/requests/on_type_formatting.rb +4 -6
  28. data/lib/ruby_lsp/requests/support/dependency_detector.rb +5 -0
  29. data/lib/ruby_lsp/requests/support/formatter_runner.rb +1 -1
  30. data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +2 -2
  31. data/lib/ruby_lsp/requests/support/rubocop_diagnostics_runner.rb +2 -3
  32. data/lib/ruby_lsp/requests/support/rubocop_formatting_runner.rb +2 -3
  33. data/lib/ruby_lsp/requests/support/syntax_tree_formatting_runner.rb +3 -2
  34. data/lib/ruby_lsp/server.rb +10 -2
  35. data/lib/ruby_lsp/setup_bundler.rb +28 -14
  36. data/lib/ruby_lsp/store.rb +20 -13
  37. data/lib/ruby_lsp/utils.rb +1 -1
  38. metadata +27 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a645362acbfb8b0f819bd89fbaf1e53d0f7d9eda81974e62bc0f114aeefc221e
4
- data.tar.gz: 861b5c68f9355f551ebfb519507ca1c0028f0e4e2edff70ae6ad62c652bdfd2c
3
+ metadata.gz: b43407d4d6309c7243a6c69aca8166f6ab3548bd47e939c016efe26c1d8a4a76
4
+ data.tar.gz: cc5f638aab3322054cc2a09b60fe22a6e5ee8196d88492b4139907c2495c7d5d
5
5
  SHA512:
6
- metadata.gz: 4283abdb1ce9865e6dbecd0827ddd88f07a63937a9f75ba8ca78142d3536624ac374b165e7a1277adee4197553c74615ed560dad3093eb346ae3e505e174dae5
7
- data.tar.gz: 6084fd63d0d780c169b8f8766f347cdb699e2490a0c8de542a3fa969ad37d7f49e281ffa7f92a7ee15a764dccac9832e5e9a49265ba9077f9d204185d0c00e64
6
+ metadata.gz: dda98d73d8ba39cdfaffa3362177ef96887eccff10332012d43f79bdc78643d3bb79a9e43e05a5c21e4550cf9b86d8d00b5742fb51b124d2ed8c8112bc6f1d1a
7
+ data.tar.gz: 25a169a3eb717f5f11b5b911c91e11e578d33734f6f616c0c92fd6ebcc89d69ddda6abb13410ef231acc398ba0472e17c778d245b89f1285d64317df98c2299c
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.7.6
1
+ 0.8.1
data/exe/ruby-lsp CHANGED
@@ -1,6 +1,46 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "optparse"
5
+
6
+ options = {}
7
+ parser = OptionParser.new do |opts|
8
+ opts.banner = "Usage: ruby-lsp [options]"
9
+
10
+ opts.on("--version", "Print ruby-lsp version") do
11
+ require "ruby-lsp"
12
+ puts RubyLsp::VERSION
13
+ exit(0)
14
+ end
15
+
16
+ opts.on("--debug", "Launch the Ruby LSP with a debugger attached") do
17
+ options[:debug] = true
18
+ end
19
+
20
+ opts.on(
21
+ "--branch [BRANCH]",
22
+ "Launch the Ruby LSP using the specified branch rather than the release version",
23
+ ) do |branch|
24
+ options[:branch] = branch
25
+ end
26
+
27
+ opts.on("-h", "--help", "Print this help") do
28
+ puts opts.help
29
+ puts
30
+ puts "See https://shopify.github.io/ruby-lsp/ for more information"
31
+ exit(0)
32
+ end
33
+ end
34
+
35
+ begin
36
+ parser.parse!
37
+ rescue OptionParser::InvalidOption => e
38
+ warn(e)
39
+ warn("")
40
+ warn(parser.help)
41
+ exit(1)
42
+ end
43
+
4
44
  # When we're running without bundler, then we need to make sure the custom bundle is fully configured and re-execute
5
45
  # using `BUNDLE_GEMFILE=.ruby-lsp/Gemfile bundle exec ruby-lsp` so that we have access to the gems that are a part of
6
46
  # the application's bundle
@@ -8,7 +48,7 @@ if ENV["BUNDLE_GEMFILE"].nil?
8
48
  require_relative "../lib/ruby_lsp/setup_bundler"
9
49
 
10
50
  begin
11
- bundle_gemfile, bundle_path = RubyLsp::SetupBundler.new(Dir.pwd).setup!
51
+ bundle_gemfile, bundle_path = RubyLsp::SetupBundler.new(Dir.pwd, branch: options[:branch]).setup!
12
52
  rescue RubyLsp::SetupBundler::BundleNotLocked
13
53
  warn("Project contains a Gemfile, but no Gemfile.lock. Run `bundle install` to lock gems and restart the server")
14
54
  exit(78)
@@ -38,38 +78,6 @@ end
38
78
 
39
79
  require_relative "../lib/ruby_lsp/internal"
40
80
 
41
- require "optparse"
42
-
43
- options = {}
44
- parser = OptionParser.new do |opts|
45
- opts.banner = "Usage: ruby-lsp [options]"
46
-
47
- opts.on("--version", "Print ruby-lsp version") do
48
- puts RubyLsp::VERSION
49
- exit(0)
50
- end
51
-
52
- opts.on("--debug", "Launch the Ruby LSP with a debugger attached") do
53
- options[:debug] = true
54
- end
55
-
56
- opts.on("-h", "--help", "Print this help") do
57
- puts opts.help
58
- puts
59
- puts "See https://shopify.github.io/ruby-lsp/ for more information"
60
- exit(0)
61
- end
62
- end
63
-
64
- begin
65
- parser.parse!
66
- rescue OptionParser::InvalidOption => e
67
- warn(e)
68
- warn("")
69
- warn(parser.help)
70
- exit(1)
71
- end
72
-
73
81
  if options[:debug]
74
82
  if ["x64-mingw-ucrt", "x64-mingw32"].include?(RUBY_PLATFORM)
75
83
  puts "Debugging is not supported on Windows"
data/exe/ruby-lsp-check CHANGED
@@ -30,13 +30,13 @@ message_queue = Thread::Queue.new
30
30
  executor = RubyLsp::Executor.new(store, message_queue)
31
31
 
32
32
  files.each_with_index do |file, index|
33
- uri = "file://#{file}"
33
+ uri = URI("file://#{file}")
34
34
  store.set(uri: uri, source: File.read(file), version: 1)
35
35
 
36
36
  # Executing any of the automatic requests will execute all of them, so here we just pick one
37
37
  result = executor.execute({
38
38
  method: "textDocument/documentSymbol",
39
- params: { textDocument: { uri: uri } },
39
+ params: { textDocument: { uri: uri.to_s } },
40
40
  })
41
41
 
42
42
  error = result.error
@@ -0,0 +1,40 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module URI
5
+ class Generic
6
+ class << self
7
+ extend T::Sig
8
+
9
+ sig { params(path: String, scheme: String).returns(URI::Generic) }
10
+ def from_path(path:, scheme: "file")
11
+ # On Windows, if the path begins with the disk name, we need to add a leading slash to make it a valid URI
12
+ escaped_path = if /^[A-Z]:/i.match?(path)
13
+ DEFAULT_PARSER.escape("/#{path}")
14
+ else
15
+ DEFAULT_PARSER.escape(path)
16
+ end
17
+
18
+ build(scheme: scheme, path: escaped_path)
19
+ end
20
+ end
21
+
22
+ extend T::Sig
23
+
24
+ sig { returns(T.nilable(String)) }
25
+ def to_standardized_path
26
+ parsed_path = path
27
+ return unless parsed_path
28
+
29
+ unescaped_path = CGI.unescape(parsed_path)
30
+
31
+ # On Windows, when we're getting the file system path back from the URI, we need to remove the leading forward
32
+ # slash
33
+ if %r{^/[A-Z]:}i.match?(unescaped_path)
34
+ unescaped_path.delete_prefix("/")
35
+ else
36
+ unescaped_path
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,91 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyIndexer
5
+ class Configuration
6
+ extend T::Sig
7
+
8
+ CONFIGURATION_SCHEMA = T.let(
9
+ {
10
+ "excluded_gems" => Array,
11
+ "included_gems" => Array,
12
+ "excluded_patterns" => Array,
13
+ "included_patterns" => Array,
14
+ }.freeze,
15
+ T::Hash[String, T::Class[Object]],
16
+ )
17
+
18
+ sig { void }
19
+ def initialize
20
+ development_only_dependencies = Bundler.definition.dependencies.filter_map do |dependency|
21
+ dependency.name if dependency.groups == [:development]
22
+ end
23
+
24
+ @excluded_gems = T.let(development_only_dependencies, T::Array[String])
25
+ @included_gems = T.let([], T::Array[String])
26
+ @excluded_patterns = T.let(["*_test.rb"], T::Array[String])
27
+ @included_patterns = T.let(["#{Dir.pwd}/**/*.rb"], T::Array[String])
28
+ end
29
+
30
+ sig { void }
31
+ def load_config
32
+ return unless File.exist?(".index.yml")
33
+
34
+ config = YAML.parse_file(".index.yml")
35
+ return unless config
36
+
37
+ config_hash = config.to_ruby
38
+ validate_config!(config_hash)
39
+ apply_config(config_hash)
40
+ rescue Psych::SyntaxError => e
41
+ raise e, "Syntax error while loading .index.yml configuration: #{e.message}"
42
+ end
43
+
44
+ sig { returns(T::Array[String]) }
45
+ def files_to_index
46
+ files_to_index = $LOAD_PATH.flat_map { |p| Dir.glob("#{p}/**/*.rb", base: p) }
47
+
48
+ @included_patterns.each do |pattern|
49
+ files_to_index.concat(Dir.glob(pattern, File::FNM_PATHNAME | File::FNM_EXTGLOB))
50
+ end
51
+
52
+ excluded_gem_paths = (@excluded_gems - @included_gems).filter_map do |gem_name|
53
+ Gem::Specification.find_by_name(gem_name).full_gem_path
54
+ rescue Gem::MissingSpecError
55
+ warn("Gem #{gem_name} is excluded in .index.yml, but that gem was not found in the bundle")
56
+ end
57
+
58
+ files_to_index.reject! do |path|
59
+ @excluded_patterns.any? { |pattern| File.fnmatch?(pattern, path, File::FNM_PATHNAME | File::FNM_EXTGLOB) } ||
60
+ excluded_gem_paths.any? { |gem_path| File.fnmatch?("#{gem_path}/**/*.rb", path) }
61
+ end
62
+ files_to_index.uniq!
63
+ files_to_index
64
+ end
65
+
66
+ private
67
+
68
+ sig { params(config: T::Hash[String, T.untyped]).void }
69
+ def validate_config!(config)
70
+ errors = config.filter_map do |key, value|
71
+ type = CONFIGURATION_SCHEMA[key]
72
+
73
+ if type.nil?
74
+ "Unknown configuration option: #{key}"
75
+ elsif !value.is_a?(type)
76
+ "Expected #{key} to be a #{type}, but got #{value.class}"
77
+ end
78
+ end
79
+
80
+ raise ArgumentError, errors.join("\n") if errors.any?
81
+ end
82
+
83
+ sig { params(config: T::Hash[String, T.untyped]).void }
84
+ def apply_config(config)
85
+ @excluded_gems.concat(config["excluded_gems"]) if config["excluded_gems"]
86
+ @included_gems.concat(config["included_gems"]) if config["included_gems"]
87
+ @excluded_patterns.concat(config["excluded_patterns"]) if config["excluded_patterns"]
88
+ @included_patterns.concat(config["included_patterns"]) if config["included_patterns"]
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,122 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyIndexer
5
+ class Index
6
+ extend T::Sig
7
+
8
+ sig { void }
9
+ def initialize
10
+ # Holds all entries in the index using the following format:
11
+ # {
12
+ # "Foo" => [#<Entry::Class>, #<Entry::Class>],
13
+ # "Foo::Bar" => [#<Entry::Class>],
14
+ # }
15
+ @entries = T.let({}, T::Hash[String, T::Array[Entry]])
16
+
17
+ # Holds references to where entries where discovered so that we can easily delete them
18
+ # {
19
+ # "/my/project/foo.rb" => [#<Entry::Class>, #<Entry::Class>],
20
+ # "/my/project/bar.rb" => [#<Entry::Class>],
21
+ # }
22
+ @files_to_entries = T.let({}, T::Hash[String, T::Array[Entry]])
23
+ end
24
+
25
+ sig { params(path: String).void }
26
+ def delete(path)
27
+ # For each constant discovered in `path`, delete the associated entry from the index. If there are no entries
28
+ # left, delete the constant from the index.
29
+ @files_to_entries[path]&.each do |entry|
30
+ entries = @entries[entry.name]
31
+ next unless entries
32
+
33
+ # Delete the specific entry from the list for this name
34
+ entries.delete(entry)
35
+ # If all entries were deleted, then remove the name from the hash
36
+ @entries.delete(entry.name) if entries.empty?
37
+ end
38
+
39
+ @files_to_entries.delete(path)
40
+ end
41
+
42
+ sig { params(entry: Entry).void }
43
+ def <<(entry)
44
+ (@entries[entry.name] ||= []) << entry
45
+ (@files_to_entries[entry.file_path] ||= []) << entry
46
+ end
47
+
48
+ sig { params(fully_qualified_name: String).returns(T.nilable(T::Array[Entry])) }
49
+ def [](fully_qualified_name)
50
+ @entries[fully_qualified_name.delete_prefix("::")]
51
+ end
52
+
53
+ # Try to find the entry based on the nesting from the most specific to the least specific. For example, if we have
54
+ # the nesting as ["Foo", "Bar"] and the name as "Baz", we will try to find it in this order:
55
+ # 1. Foo::Bar::Baz
56
+ # 2. Foo::Baz
57
+ # 3. Baz
58
+ sig { params(name: String, nesting: T::Array[String]).returns(T.nilable(T::Array[Entry])) }
59
+ def resolve(name, nesting)
60
+ (nesting.length + 1).downto(0).each do |i|
61
+ prefix = T.must(nesting[0...i]).join("::")
62
+ full_name = prefix.empty? ? name : "#{prefix}::#{name}"
63
+ entries = @entries[full_name]
64
+ return entries if entries
65
+ end
66
+
67
+ nil
68
+ end
69
+
70
+ sig { params(paths: T::Array[String]).void }
71
+ def index_all(paths: RubyIndexer.configuration.files_to_index)
72
+ paths.each { |path| index_single(path) }
73
+ end
74
+
75
+ sig { params(path: String, source: T.nilable(String)).void }
76
+ def index_single(path, source = nil)
77
+ content = source || File.read(path)
78
+ visitor = IndexVisitor.new(self, YARP.parse(content), path)
79
+ visitor.run
80
+ end
81
+
82
+ class Entry
83
+ extend T::Sig
84
+
85
+ sig { returns(String) }
86
+ attr_reader :name
87
+
88
+ sig { returns(String) }
89
+ attr_reader :file_path
90
+
91
+ sig { returns(YARP::Location) }
92
+ attr_reader :location
93
+
94
+ sig { returns(T::Array[String]) }
95
+ attr_reader :comments
96
+
97
+ sig { params(name: String, file_path: String, location: YARP::Location, comments: T::Array[String]).void }
98
+ def initialize(name, file_path, location, comments)
99
+ @name = name
100
+ @file_path = file_path
101
+ @location = location
102
+ @comments = comments
103
+ end
104
+
105
+ class Namespace < Entry
106
+ sig { returns(String) }
107
+ def short_name
108
+ T.must(@name.split("::").last)
109
+ end
110
+ end
111
+
112
+ class Module < Namespace
113
+ end
114
+
115
+ class Class < Namespace
116
+ end
117
+
118
+ class Constant < Entry
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,121 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyIndexer
5
+ class IndexVisitor < YARP::Visitor
6
+ extend T::Sig
7
+
8
+ sig { params(index: Index, parse_result: YARP::ParseResult, file_path: String).void }
9
+ def initialize(index, parse_result, file_path)
10
+ @index = index
11
+ @parse_result = parse_result
12
+ @file_path = file_path
13
+ @stack = T.let([], T::Array[String])
14
+ @comments_by_line = T.let(
15
+ parse_result.comments.to_h do |c|
16
+ [c.location.start_line, c]
17
+ end,
18
+ T::Hash[Integer, YARP::Comment],
19
+ )
20
+
21
+ super()
22
+ end
23
+
24
+ sig { void }
25
+ def run
26
+ visit(@parse_result.value)
27
+ end
28
+
29
+ sig { params(node: T.nilable(YARP::Node)).void }
30
+ def visit(node)
31
+ case node
32
+ when YARP::ProgramNode, YARP::StatementsNode
33
+ visit_child_nodes(node)
34
+ when YARP::ClassNode
35
+ add_index_entry(node, Index::Entry::Class)
36
+ when YARP::ModuleNode
37
+ add_index_entry(node, Index::Entry::Module)
38
+ when YARP::ConstantWriteNode, YARP::ConstantOperatorOrWriteNode
39
+ add_constant(node)
40
+ when YARP::ConstantPathWriteNode, YARP::ConstantPathOperatorOrWriteNode
41
+ add_constant_with_path(node)
42
+ end
43
+ end
44
+
45
+ # Override to avoid using `map` instead of `each`
46
+ sig { params(nodes: T::Array[T.nilable(YARP::Node)]).void }
47
+ def visit_all(nodes)
48
+ nodes.each { |node| visit(node) }
49
+ end
50
+
51
+ private
52
+
53
+ sig do
54
+ params(
55
+ node: T.any(YARP::ConstantWriteNode, YARP::ConstantOperatorOrWriteNode),
56
+ ).void
57
+ end
58
+ def add_constant(node)
59
+ comments = collect_comments(node)
60
+ @index << Index::Entry::Constant.new(fully_qualify_name(node.name), @file_path, node.location, comments)
61
+ end
62
+
63
+ sig do
64
+ params(
65
+ node: T.any(YARP::ConstantPathWriteNode, YARP::ConstantPathOperatorOrWriteNode),
66
+ ).void
67
+ end
68
+ def add_constant_with_path(node)
69
+ # ignore variable constants like `var::FOO` or `self.class::FOO`
70
+ return unless node.target.parent.nil? || node.target.parent.is_a?(YARP::ConstantReadNode)
71
+
72
+ name = node.target.location.slice
73
+ fully_qualified_name = name.start_with?("::") ? name.delete_prefix("::") : fully_qualify_name(name)
74
+
75
+ comments = collect_comments(node)
76
+ @index << Index::Entry::Constant.new(fully_qualified_name, @file_path, node.location, comments)
77
+ end
78
+
79
+ sig { params(node: T.any(YARP::ClassNode, YARP::ModuleNode), klass: T.class_of(Index::Entry)).void }
80
+ def add_index_entry(node, klass)
81
+ name = node.constant_path.location.slice
82
+
83
+ unless /^[A-Z:]/.match?(name)
84
+ return visit_child_nodes(node)
85
+ end
86
+
87
+ fully_qualified_name = name.start_with?("::") ? name : fully_qualify_name(name)
88
+ name.delete_prefix!("::")
89
+
90
+ comments = collect_comments(node)
91
+ @index << klass.new(fully_qualified_name, @file_path, node.location, comments)
92
+ @stack << name
93
+ visit_child_nodes(node)
94
+ @stack.pop
95
+ end
96
+
97
+ sig { params(node: YARP::Node).returns(T::Array[String]) }
98
+ def collect_comments(node)
99
+ comments = []
100
+
101
+ start_line = node.location.start_line - 1
102
+ start_line -= 1 unless @comments_by_line.key?(start_line)
103
+
104
+ start_line.downto(1) do |line|
105
+ comment = @comments_by_line[line]
106
+ break unless comment
107
+
108
+ comments.unshift(comment.location.slice)
109
+ end
110
+
111
+ comments
112
+ end
113
+
114
+ sig { params(name: String).returns(String) }
115
+ def fully_qualify_name(name)
116
+ return name if @stack.empty?
117
+
118
+ "#{@stack.join("::")}::#{name}"
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,19 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "yaml"
5
+
6
+ require "ruby_indexer/lib/ruby_indexer/visitor"
7
+ require "ruby_indexer/lib/ruby_indexer/index"
8
+ require "ruby_indexer/lib/ruby_indexer/configuration"
9
+
10
+ module RubyIndexer
11
+ class << self
12
+ extend T::Sig
13
+
14
+ sig { returns(Configuration) }
15
+ def configuration
16
+ @configuration ||= T.let(Configuration.new, T.nilable(Configuration))
17
+ end
18
+ end
19
+ end