ruby-lsp 0.7.6 → 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
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