ruby-lsp 0.26.9 → 0.27.0.beta2

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/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +2 -2
  4. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +8 -8
  5. data/lib/ruby_indexer/ruby_indexer.rb +0 -1
  6. data/lib/ruby_lsp/addon.rb +19 -19
  7. data/lib/ruby_lsp/global_state.rb +11 -2
  8. data/lib/ruby_lsp/internal.rb +6 -1
  9. data/lib/ruby_lsp/listeners/definition.rb +65 -99
  10. data/lib/ruby_lsp/listeners/document_link.rb +4 -0
  11. data/lib/ruby_lsp/listeners/hover.rb +258 -123
  12. data/lib/ruby_lsp/listeners/spec_style.rb +6 -1
  13. data/lib/ruby_lsp/listeners/test_discovery.rb +21 -14
  14. data/lib/ruby_lsp/listeners/test_style.rb +20 -8
  15. data/lib/ruby_lsp/node_context.rb +32 -9
  16. data/lib/ruby_lsp/requests/completion_resolve.rb +9 -13
  17. data/lib/ruby_lsp/requests/discover_tests.rb +5 -41
  18. data/lib/ruby_lsp/requests/hover.rb +2 -5
  19. data/lib/ruby_lsp/requests/on_type_formatting.rb +4 -0
  20. data/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +66 -22
  21. data/lib/ruby_lsp/requests/references.rb +170 -70
  22. data/lib/ruby_lsp/requests/rename.rb +64 -72
  23. data/lib/ruby_lsp/requests/request.rb +3 -33
  24. data/lib/ruby_lsp/requests/support/common.rb +53 -0
  25. data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +82 -46
  26. data/lib/ruby_lsp/requests/workspace_symbol.rb +15 -37
  27. data/lib/ruby_lsp/rubydex/declaration.rb +48 -0
  28. data/lib/ruby_lsp/rubydex/definition.rb +257 -0
  29. data/lib/ruby_lsp/rubydex/reference.rb +21 -0
  30. data/lib/ruby_lsp/server.rb +82 -8
  31. data/lib/ruby_lsp/store.rb +0 -6
  32. data/lib/ruby_lsp/test_helper.rb +3 -0
  33. data/lib/ruby_lsp/type_inferrer.rb +111 -31
  34. metadata +18 -5
  35. data/lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb +0 -335
  36. data/lib/ruby_lsp/static_docs.rb +0 -20
  37. data/static_docs/break.md +0 -103
  38. data/static_docs/yield.md +0 -81
@@ -22,6 +22,7 @@ module RubyLsp
22
22
  def initialize(global_state, store, document, params)
23
23
  super()
24
24
  @global_state = global_state
25
+ @graph = global_state.graph #: Rubydex::Graph
25
26
  @store = store
26
27
  @document = document
27
28
  @position = params[:position] #: Hash[Symbol, Integer]
@@ -56,17 +57,14 @@ module RubyLsp
56
57
  name = RubyIndexer::Index.constant_name(target)
57
58
  return unless name
58
59
 
59
- entries = @global_state.index.resolve(name, node_context.nesting)
60
- return unless entries
60
+ declaration = @graph.resolve_constant(name, node_context.nesting)
61
+ return unless declaration
61
62
 
62
- if (conflict_entries = @global_state.index.resolve(@new_name, node_context.nesting))
63
- raise InvalidNameError, "The new name is already in use by #{conflict_entries.first&.name}"
63
+ if (conflict = @graph.resolve_constant(@new_name, node_context.nesting))
64
+ raise InvalidNameError, "The new name is already in use by #{conflict.name}"
64
65
  end
65
66
 
66
- fully_qualified_name = entries.first #: as !nil
67
- .name
68
- reference_target = RubyIndexer::ReferenceFinder::ConstTarget.new(fully_qualified_name)
69
- changes = collect_text_edits(reference_target, name)
67
+ changes = collect_text_edits(declaration, name)
70
68
 
71
69
  # If the client doesn't support resource operations, such as renaming files, then we can only return the basic
72
70
  # text changes
@@ -78,99 +76,93 @@ module RubyLsp
78
76
  # renamed and then the URI associated to the text edit no longer exists, causing it to be dropped
79
77
  document_changes = changes.map do |uri, edits|
80
78
  Interface::TextDocumentEdit.new(
81
- text_document: Interface::VersionedTextDocumentIdentifier.new(uri: uri, version: nil),
79
+ text_document: Interface::OptionalVersionedTextDocumentIdentifier.new(uri: uri, version: nil),
82
80
  edits: edits,
83
81
  )
84
82
  end
85
83
 
86
- collect_file_renames(fully_qualified_name, document_changes)
84
+ collect_file_renames(declaration, document_changes)
87
85
  Interface::WorkspaceEdit.new(document_changes: document_changes)
88
86
  end
89
87
 
90
88
  private
91
89
 
92
- #: (String fully_qualified_name, Array[(Interface::RenameFile | Interface::TextDocumentEdit)] document_changes) -> void
93
- def collect_file_renames(fully_qualified_name, document_changes)
90
+ #: (Rubydex::Declaration, Array[(Interface::RenameFile | Interface::TextDocumentEdit)]) -> void
91
+ def collect_file_renames(declaration, document_changes)
94
92
  # Check if the declarations of the symbol being renamed match the file name. In case they do, we automatically
95
93
  # rename the files for the user.
96
94
  #
97
95
  # We also look for an associated test file and rename it too
98
- short_name = fully_qualified_name.split("::").last #: as !nil
99
96
 
100
- @global_state.index[fully_qualified_name]&.each do |entry|
97
+ unless [
98
+ Rubydex::Class,
99
+ Rubydex::Module,
100
+ Rubydex::Constant,
101
+ Rubydex::ConstantAlias,
102
+ ].any? { |type| declaration.is_a?(type) }
103
+ return
104
+ end
105
+
106
+ short_name = declaration.unqualified_name
107
+
108
+ declaration.definitions.each do |definition|
101
109
  # Do not rename files that are not part of the workspace
102
- uri = entry.uri
110
+ uri = URI(definition.location.uri)
103
111
  file_path = uri.full_path
104
112
  next unless file_path&.start_with?(@global_state.workspace_path)
105
113
 
106
- case entry
107
- when RubyIndexer::Entry::Class, RubyIndexer::Entry::Module, RubyIndexer::Entry::Constant,
108
- RubyIndexer::Entry::ConstantAlias, RubyIndexer::Entry::UnresolvedConstantAlias
109
-
110
- file_name = file_from_constant_name(short_name)
114
+ file_name = file_from_constant_name(short_name)
115
+ next unless "#{file_name}.rb" == File.basename(file_path)
111
116
 
112
- if "#{file_name}.rb" == entry.file_name
113
- new_file_name = file_from_constant_name(
114
- @new_name.split("::").last, #: as !nil
115
- )
117
+ new_file_name = file_from_constant_name(
118
+ @new_name.split("::").last, #: as !nil
119
+ )
116
120
 
117
- new_uri = URI::Generic.from_path(path: File.join(
118
- File.dirname(file_path),
119
- "#{new_file_name}.rb",
120
- )).to_s
121
+ new_uri = URI::Generic.from_path(path: File.join(
122
+ File.dirname(file_path),
123
+ "#{new_file_name}.rb",
124
+ )).to_s
121
125
 
122
- document_changes << Interface::RenameFile.new(kind: "rename", old_uri: uri.to_s, new_uri: new_uri)
123
- end
124
- end
126
+ document_changes << Interface::RenameFile.new(kind: "rename", old_uri: uri.to_s, new_uri: new_uri)
125
127
  end
126
128
  end
127
129
 
128
- #: (RubyIndexer::ReferenceFinder::Target target, String name) -> Hash[String, Array[Interface::TextEdit]]
129
- def collect_text_edits(target, name)
130
- changes = {}
131
-
132
- Dir.glob(File.join(@global_state.workspace_path, "**/*.rb")).each do |path|
133
- uri = URI::Generic.from_path(path: path)
134
- # If the document is being managed by the client, then we should use whatever is present in the store instead
135
- # of reading from disk
136
- next if @store.key?(uri)
137
-
138
- parse_result = Prism.parse_file(path)
139
- edits = collect_changes(target, parse_result.value, name, uri)
140
- changes[uri.to_s] = edits unless edits.empty?
141
- rescue Errno::EISDIR, Errno::ENOENT
142
- # If `path` is a directory, just ignore it and continue. If the file doesn't exist, then we also ignore it.
143
- end
144
-
145
- @store.each do |uri, document|
146
- next unless document.is_a?(RubyDocument) || document.is_a?(ERBDocument)
130
+ #: (Rubydex::Declaration declaration, String name) -> Hash[String, Array[Interface::TextEdit]]
131
+ def collect_text_edits(declaration, name)
132
+ changes = {} #: Hash[String, Array[Interface::TextEdit]]
133
+ short_name = name.split("::").last #: as !nil
134
+ new_short_name = @new_name.split("::").last #: as !nil
135
+
136
+ # Collect edits for definition sites (where the constant is declared)
137
+ declaration.definitions.each do |definition|
138
+ name_loc = definition.name_location
139
+ next unless name_loc
140
+
141
+ uri_string = name_loc.uri
142
+ edits = (changes[uri_string] ||= [])
143
+
144
+ # The name_location spans the constant name as written in the definition.
145
+ # We only replace the unqualified name portion (the last segment).
146
+ range = Interface::Range.new(
147
+ start: Interface::Position.new(
148
+ line: name_loc.end_line,
149
+ character: name_loc.end_column - short_name.length,
150
+ ),
151
+ end: Interface::Position.new(line: name_loc.end_line, character: name_loc.end_column),
152
+ )
147
153
 
148
- edits = collect_changes(target, document.ast, name, document.uri)
149
- changes[uri] = edits unless edits.empty?
154
+ edits << Interface::TextEdit.new(range: range, new_text: new_short_name)
150
155
  end
151
156
 
152
- changes
153
- end
154
-
155
- #: (RubyIndexer::ReferenceFinder::Target target, Prism::Node ast, String name, URI::Generic uri) -> Array[Interface::TextEdit]
156
- def collect_changes(target, ast, name, uri)
157
- dispatcher = Prism::Dispatcher.new
158
- finder = RubyIndexer::ReferenceFinder.new(target, @global_state.index, dispatcher, uri)
159
- dispatcher.visit(ast)
160
-
161
- finder.references.map do |reference|
162
- adjust_reference_for_edit(name, reference)
157
+ # Collect edits for reference sites (where the constant is used)
158
+ declaration.references.each do |reference|
159
+ ref = reference #: as Rubydex::ConstantReference
160
+ uri_string = ref.location.uri
161
+ edits = (changes[uri_string] ||= [])
162
+ edits << Interface::TextEdit.new(range: ref.to_lsp_range, new_text: new_short_name)
163
163
  end
164
- end
165
-
166
- #: (String name, RubyIndexer::ReferenceFinder::Reference reference) -> Interface::TextEdit
167
- def adjust_reference_for_edit(name, reference)
168
- # The reference may include a namespace in front. We need to check if the rename new name includes namespaces
169
- # and then adjust both the text and the location to produce the correct edit
170
- location = reference.location
171
- new_text = reference.name.sub(name, @new_name)
172
164
 
173
- Interface::TextEdit.new(range: range_from_location(location), new_text: new_text)
165
+ changes
174
166
  end
175
167
 
176
168
  #: (String constant_name) -> String
@@ -5,6 +5,8 @@ module RubyLsp
5
5
  module Requests
6
6
  # @abstract
7
7
  class Request
8
+ include Support::Common
9
+
8
10
  class InvalidFormatter < StandardError; end
9
11
 
10
12
  # @abstract
@@ -26,24 +28,6 @@ module RubyLsp
26
28
  end
27
29
  end
28
30
 
29
- # Checks if a location covers a position
30
- #: (Prism::Location location, untyped position) -> bool
31
- def cover?(location, position)
32
- start_covered =
33
- location.start_line - 1 < position[:line] ||
34
- (
35
- location.start_line - 1 == position[:line] &&
36
- location.start_column <= position[:character]
37
- )
38
- end_covered =
39
- location.end_line - 1 > position[:line] ||
40
- (
41
- location.end_line - 1 == position[:line] &&
42
- location.end_column >= position[:character]
43
- )
44
- start_covered && end_covered
45
- end
46
-
47
31
  # Based on a constant node target, a constant path node parent and a position, this method will find the exact
48
32
  # portion of the constant path that matches the requested position, for higher precision in hover and
49
33
  # definition. For example:
@@ -62,27 +46,13 @@ module RubyLsp
62
46
  parent = target #: as Prism::ConstantPathNode
63
47
  .parent #: Prism::Node?
64
48
 
65
- while parent && cover?(parent.location, position)
49
+ while parent && covers_position?(parent.location, position)
66
50
  target = parent
67
51
  parent = target.is_a?(Prism::ConstantPathNode) ? target.parent : nil
68
52
  end
69
53
 
70
54
  target
71
55
  end
72
-
73
- # Checks if a given location covers the position requested
74
- #: (Prism::Location? location, Hash[Symbol, untyped] position) -> bool
75
- def covers_position?(location, position)
76
- return false unless location
77
-
78
- start_line = location.start_line - 1
79
- end_line = location.end_line - 1
80
- line = position[:line]
81
- character = position[:character]
82
-
83
- (start_line < line || (start_line == line && location.start_column <= character)) &&
84
- (end_line > line || (end_line == line && location.end_column >= character))
85
- end
86
56
  end
87
57
  end
88
58
  end
@@ -34,6 +34,19 @@ module RubyLsp
34
34
  )
35
35
  end
36
36
 
37
+ #: (Prism::Location? location, Hash[Symbol, untyped] position) -> bool
38
+ def covers_position?(location, position)
39
+ return false unless location
40
+
41
+ start_line = location.start_line - 1
42
+ end_line = location.end_line - 1
43
+ line = position[:line]
44
+ character = position[:character]
45
+
46
+ (start_line < line || (start_line == line && location.start_column <= character)) &&
47
+ (end_line > line || (end_line == line && location.end_column >= character))
48
+ end
49
+
37
50
  #: (Prism::Node node, title: String, command_name: String, arguments: Array[untyped]?, data: Hash[untyped, untyped]?) -> Interface::CodeLens
38
51
  def create_code_lens(node, title:, command_name:, arguments:, data:)
39
52
  range = range_from_node(node)
@@ -64,6 +77,46 @@ module RubyLsp
64
77
  receiver.nil? || receiver.is_a?(Prism::SelfNode)
65
78
  end
66
79
 
80
+ #: (String, Enumerable[Rubydex::Definition], ?Integer?) -> Hash[Symbol, String]
81
+ def categorized_markdown_from_definitions(title, definitions, max_entries = nil)
82
+ markdown_title = "```ruby\n#{title}\n```"
83
+ file_links = []
84
+ content = +""
85
+ defs = max_entries ? definitions.take(max_entries) : definitions
86
+ defs.each do |definition|
87
+ # For Markdown links, we need 1 based display locations
88
+ loc = definition.location.to_display
89
+ uri = URI(loc.uri)
90
+ file_name = if uri.scheme == "untitled"
91
+ uri.opaque #: as !nil
92
+ else
93
+ File.basename(
94
+ uri.full_path, #: as !nil
95
+ )
96
+ end
97
+
98
+ # The format for VS Code file URIs is `file:///path/to/file.rb#Lstart_line,start_column-end_line,end_column`
99
+ string_uri = "#{loc.uri}#L#{loc.start_line},#{loc.start_column}-#{loc.end_line},#{loc.end_column}"
100
+ file_links << "[#{file_name}](#{string_uri})"
101
+ content << "\n\n#{definition.comments.map { |comment| comment.string.delete_prefix("# ") }.join("\n")}" unless definition.comments.empty?
102
+ end
103
+
104
+ total_definitions = definitions.count
105
+
106
+ additional_entries_text = if max_entries && total_definitions > max_entries
107
+ additional = total_definitions - max_entries
108
+ " | #{additional} other#{additional > 1 ? "s" : ""}"
109
+ else
110
+ ""
111
+ end
112
+
113
+ {
114
+ title: markdown_title,
115
+ links: "**Definitions**: #{file_links.join(" | ")}#{additional_entries_text}",
116
+ documentation: content,
117
+ }
118
+ end
119
+
67
120
  #: (String title, (Array[RubyIndexer::Entry] | RubyIndexer::Entry) entries, ?Integer? max_entries) -> Hash[Symbol, String]
68
121
  def categorized_markdown_from_index_entries(title, entries, max_entries = nil)
69
122
  markdown_title = "```ruby\n#{title}\n```"
@@ -9,65 +9,101 @@ module RubyLsp
9
9
  class TypeHierarchySupertypes < Request
10
10
  include Support::Common
11
11
 
12
- #: (RubyIndexer::Index index, Hash[Symbol, untyped] item) -> void
13
- def initialize(index, item)
12
+ #: (GlobalState, Hash[Symbol, untyped]) -> void
13
+ def initialize(global_state, item)
14
14
  super()
15
15
 
16
- @index = index
16
+ @graph = global_state.graph #: Rubydex::Graph
17
17
  @item = item
18
18
  end
19
19
 
20
20
  # @override
21
21
  #: -> Array[Interface::TypeHierarchyItem]?
22
22
  def perform
23
- name = @item[:name]
24
- entries = @index[name]
25
-
26
- parents = Set.new #: Set[RubyIndexer::Entry::Namespace]
27
- return unless entries&.any?
28
-
29
- entries.each do |entry|
30
- next unless entry.is_a?(RubyIndexer::Entry::Namespace)
31
-
32
- if entry.is_a?(RubyIndexer::Entry::Class)
33
- parent_class_name = entry.parent_class
34
- if parent_class_name
35
- resolved_parent_entries = @index.resolve(parent_class_name, entry.nesting)
36
- resolved_parent_entries&.each do |entry|
37
- next unless entry.is_a?(RubyIndexer::Entry::Class)
38
-
39
- parents << entry
40
- end
41
- end
42
- end
43
-
44
- entry.mixin_operations.each do |mixin_operation|
45
- mixin_name = mixin_operation.module_name
46
- resolved_mixin_entries = @index.resolve(mixin_name, entry.nesting)
47
- next unless resolved_mixin_entries
48
-
49
- resolved_mixin_entries.each do |mixin_entry|
50
- next unless mixin_entry.is_a?(RubyIndexer::Entry::Module)
51
-
52
- parents << mixin_entry
53
- end
54
- end
55
- end
23
+ fully_qualified_name = @item.dig(:data, :fully_qualified_name) || @item[:name] #: String?
24
+ return unless fully_qualified_name
25
+
26
+ declaration = @graph[fully_qualified_name]
27
+ return unless declaration.is_a?(Rubydex::Namespace)
56
28
 
57
- parents.map { |entry| hierarchy_item(entry) }
29
+ compute_supertypes(declaration).filter_map { |name, backing| hierarchy_item(name, backing) }
58
30
  end
59
31
 
60
32
  private
61
33
 
62
- #: (RubyIndexer::Entry entry) -> Interface::TypeHierarchyItem
63
- def hierarchy_item(entry)
64
- Interface::TypeHierarchyItem.new(
65
- name: entry.name,
66
- kind: kind_for_entry(entry),
67
- uri: entry.uri.to_s,
68
- range: range_from_location(entry.location),
69
- selection_range: range_from_location(entry.name_location),
70
- detail: entry.file_name,
34
+ # Returns an array of `[display_name, backing_declaration]` pairs. `display_name` is the name shown in the type
35
+ # hierarchy item (which may be a synthesized singleton class name like `Object::<Object>`). `backing_declaration`
36
+ # is the namespace whose primary definition provides the location for the hierarchy item — it may differ from the
37
+ # display name when the singleton class is implicit and has no definitions of its own, in which case we fall back
38
+ # to the attached object's definition so the user still lands somewhere useful.
39
+ #
40
+ #: (Rubydex::Namespace) -> Array[[String, Rubydex::Namespace]]
41
+ def compute_supertypes(declaration)
42
+ case declaration
43
+ when Rubydex::SingletonClass
44
+ singleton_supertypes(declaration)
45
+ when Rubydex::Class
46
+ class_supertypes(declaration)
47
+ else
48
+ explicit_supertypes(declaration)
49
+ end
50
+ end
51
+
52
+ #: (Rubydex::Class) -> Array[[String, Rubydex::Namespace]]
53
+ def class_supertypes(declaration)
54
+ # `BasicObject` is the root of the Ruby class hierarchy
55
+ supertypes = explicit_supertypes(declaration)
56
+ return supertypes if declaration.name == "BasicObject"
57
+
58
+ # If the class has any superclass reference (resolved or unresolved), don't re-add the implicit `Object`.
59
+ has_superclass = declaration.definitions.any? do |d|
60
+ d.is_a?(Rubydex::ClassDefinition) && !d.superclass.nil?
61
+ end
62
+ return supertypes if has_superclass
63
+
64
+ object = @graph["Object"] #: as Rubydex::Namespace
65
+ supertypes << ["Object", object]
66
+ supertypes
67
+ end
68
+
69
+ #: (Rubydex::Namespace) -> Array[[String, Rubydex::Namespace]]
70
+ def explicit_supertypes(declaration)
71
+ declaration.direct_supertypes.map { |s| [s.name, s] }
72
+ end
73
+
74
+ # Singleton classes don't have their own superclass references. Their direct supertype is the singleton class of
75
+ # the attached object's superclass, computed recursively so that nested singleton classes (e.g.
76
+ # `Foo::<Foo>::<<Foo>>`) still resolve to the matching depth on the parent chain. When the synthesized singleton
77
+ # class name has no backing declaration with definitions (implicit singleton), we fall back to the attached
78
+ # supertype's backing so the user is still navigated to a meaningful location.
79
+ #
80
+ #: (Rubydex::SingletonClass) -> Array[[String, Rubydex::Namespace]]
81
+ def singleton_supertypes(declaration)
82
+ attached = declaration.owner
83
+ return [] unless attached.is_a?(Rubydex::Namespace)
84
+
85
+ compute_supertypes(attached).map do |parent_name, parent_backing|
86
+ singleton_name = singleton_name_of(parent_name)
87
+ found = @graph[singleton_name]
88
+ backing = found.is_a?(Rubydex::Namespace) && found.definitions.any? ? found : parent_backing
89
+ [singleton_name, backing]
90
+ end
91
+ end
92
+
93
+ #: (String) -> String
94
+ def singleton_name_of(name)
95
+ unqualified = name.split("::").last || name
96
+ "#{name}::<#{unqualified}>"
97
+ end
98
+
99
+ #: (String, Rubydex::Namespace) -> Interface::TypeHierarchyItem?
100
+ def hierarchy_item(name, declaration)
101
+ primary = declaration.definitions.first #: Rubydex::Definition?
102
+ return unless primary
103
+
104
+ primary.to_lsp_type_hierarchy_item(
105
+ name,
106
+ detail: declaration.lsp_type_hierarchy_detail,
71
107
  )
72
108
  end
73
109
  end
@@ -12,54 +12,32 @@ module RubyLsp
12
12
  #: (GlobalState global_state, String? query) -> void
13
13
  def initialize(global_state, query)
14
14
  super()
15
- @global_state = global_state
16
15
  @query = query
17
- @index = global_state.index #: RubyIndexer::Index
16
+ @graph = global_state.graph #: Rubydex::Graph
18
17
  end
19
18
 
20
19
  # @override
21
20
  #: -> Array[Interface::WorkspaceSymbol]
22
21
  def perform
23
- fuzzy_search.filter_map do |entry|
24
- kind = kind_for_entry(entry)
25
- loc = entry.location
22
+ response = []
26
23
 
27
- # We use the namespace as the container name, but we also use the full name as the regular name. The reason we
28
- # do this is to allow people to search for fully qualified names (e.g.: `Foo::Bar`). If we only included the
29
- # short name `Bar`, then searching for `Foo::Bar` would not return any results
30
- *container, _short_name = entry.name.split("::")
24
+ @graph.fuzzy_search(@query || "").each do |declaration|
25
+ name = declaration.name
31
26
 
32
- Interface::WorkspaceSymbol.new(
33
- name: entry.name,
34
- container_name: container.join("::"),
35
- kind: kind,
36
- location: Interface::Location.new(
37
- uri: entry.uri.to_s,
38
- range: Interface::Range.new(
39
- start: Interface::Position.new(line: loc.start_line - 1, character: loc.start_column),
40
- end: Interface::Position.new(line: loc.end_line - 1, character: loc.end_column),
41
- ),
42
- ),
43
- )
44
- end
45
- end
46
-
47
- private
27
+ declaration.definitions.each do |definition|
28
+ location = definition.location
29
+ uri = URI(location.uri)
30
+ file_path = uri.full_path
48
31
 
49
- #: -> Array[RubyIndexer::Entry]
50
- def fuzzy_search
51
- @index.fuzzy_search(@query) do |entry|
52
- file_path = entry.uri.full_path
32
+ # We only show symbols declared in the workspace
33
+ in_dependencies = file_path && !not_in_dependencies?(file_path)
34
+ next if in_dependencies
53
35
 
54
- # We only show symbols declared in the workspace
55
- in_dependencies = file_path && !not_in_dependencies?(file_path)
56
- next if in_dependencies
57
-
58
- # We should never show private symbols when searching the entire workspace
59
- next if entry.private?
60
-
61
- true
36
+ response << definition.to_lsp_workspace_symbol(name)
37
+ end
62
38
  end
39
+
40
+ response
63
41
  end
64
42
  end
65
43
  end
@@ -0,0 +1,48 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Rubydex
5
+ class Declaration
6
+ # Detail text shown on a `TypeHierarchyItem` for this declaration. Hints at multiplicity
7
+ # when the declaration spans more than one re-open; otherwise falls back to the primary
8
+ # definition's file name so users can quickly see where the type comes from.
9
+ #
10
+ #: () -> String?
11
+ def lsp_type_hierarchy_detail
12
+ defs = definitions
13
+ count = defs.count
14
+ return "#{count} definitions" if count > 1
15
+
16
+ primary = defs.first
17
+ return unless primary
18
+
19
+ uri = URI(primary.location.uri)
20
+ path = uri.full_path
21
+ path ? File.basename(path) : uri.to_s
22
+ end
23
+ end
24
+
25
+ class Namespace
26
+ # Resolved, deduplicated direct supertypes across every re-open of this declaration.
27
+ # Aggregates each definition's own `superclass`/`include`/`prepend` references and drops
28
+ # unresolved ones. Order is stable (first-seen across definitions).
29
+ #: () -> Array[Rubydex::Namespace]
30
+ def direct_supertypes
31
+ seen = {} #: Hash[String, Rubydex::Namespace]
32
+
33
+ definitions.each do |definition|
34
+ definition.direct_supertype_references.each do |ref|
35
+ next unless ref.is_a?(ResolvedConstantReference)
36
+
37
+ target = ref.declaration
38
+ next unless target.is_a?(Namespace)
39
+ next if seen.key?(target.name)
40
+
41
+ seen[target.name] = target
42
+ end
43
+ end
44
+
45
+ seen.values
46
+ end
47
+ end
48
+ end