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.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +2 -2
- data/lib/ruby_indexer/lib/ruby_indexer/index.rb +8 -8
- data/lib/ruby_indexer/ruby_indexer.rb +0 -1
- data/lib/ruby_lsp/addon.rb +19 -19
- data/lib/ruby_lsp/global_state.rb +11 -2
- data/lib/ruby_lsp/internal.rb +6 -1
- data/lib/ruby_lsp/listeners/definition.rb +65 -99
- data/lib/ruby_lsp/listeners/document_link.rb +4 -0
- data/lib/ruby_lsp/listeners/hover.rb +258 -123
- data/lib/ruby_lsp/listeners/spec_style.rb +6 -1
- data/lib/ruby_lsp/listeners/test_discovery.rb +21 -14
- data/lib/ruby_lsp/listeners/test_style.rb +20 -8
- data/lib/ruby_lsp/node_context.rb +32 -9
- data/lib/ruby_lsp/requests/completion_resolve.rb +9 -13
- data/lib/ruby_lsp/requests/discover_tests.rb +5 -41
- data/lib/ruby_lsp/requests/hover.rb +2 -5
- data/lib/ruby_lsp/requests/on_type_formatting.rb +4 -0
- data/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +66 -22
- data/lib/ruby_lsp/requests/references.rb +170 -70
- data/lib/ruby_lsp/requests/rename.rb +64 -72
- data/lib/ruby_lsp/requests/request.rb +3 -33
- data/lib/ruby_lsp/requests/support/common.rb +53 -0
- data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +82 -46
- data/lib/ruby_lsp/requests/workspace_symbol.rb +15 -37
- data/lib/ruby_lsp/rubydex/declaration.rb +48 -0
- data/lib/ruby_lsp/rubydex/definition.rb +257 -0
- data/lib/ruby_lsp/rubydex/reference.rb +21 -0
- data/lib/ruby_lsp/server.rb +82 -8
- data/lib/ruby_lsp/store.rb +0 -6
- data/lib/ruby_lsp/test_helper.rb +3 -0
- data/lib/ruby_lsp/type_inferrer.rb +111 -31
- metadata +18 -5
- data/lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb +0 -335
- data/lib/ruby_lsp/static_docs.rb +0 -20
- data/static_docs/break.md +0 -103
- 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
|
-
|
|
60
|
-
return unless
|
|
60
|
+
declaration = @graph.resolve_constant(name, node_context.nesting)
|
|
61
|
+
return unless declaration
|
|
61
62
|
|
|
62
|
-
if (
|
|
63
|
-
raise InvalidNameError, "The new name is already in use by #{
|
|
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
|
-
|
|
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::
|
|
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(
|
|
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
|
-
#: (
|
|
93
|
-
def collect_file_renames(
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
)
|
|
117
|
+
new_file_name = file_from_constant_name(
|
|
118
|
+
@new_name.split("::").last, #: as !nil
|
|
119
|
+
)
|
|
116
120
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
#: (
|
|
129
|
-
def collect_text_edits(
|
|
130
|
-
changes = {}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
changes[
|
|
141
|
-
|
|
142
|
-
#
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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 &&
|
|
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
|
-
#: (
|
|
13
|
-
def initialize(
|
|
12
|
+
#: (GlobalState, Hash[Symbol, untyped]) -> void
|
|
13
|
+
def initialize(global_state, item)
|
|
14
14
|
super()
|
|
15
15
|
|
|
16
|
-
@
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return unless
|
|
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
|
-
|
|
29
|
+
compute_supertypes(declaration).filter_map { |name, backing| hierarchy_item(name, backing) }
|
|
58
30
|
end
|
|
59
31
|
|
|
60
32
|
private
|
|
61
33
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
@
|
|
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
|
-
|
|
24
|
-
kind = kind_for_entry(entry)
|
|
25
|
-
loc = entry.location
|
|
22
|
+
response = []
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|