Skip to content

Commit 46c23bc

Browse files
committed
Migrate rename to use Rubydex
1 parent 2837bc0 commit 46c23bc

4 files changed

Lines changed: 234 additions & 153 deletions

File tree

lib/ruby_lsp/internal.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
# Rubydex LSP additions
3535
require "ruby_lsp/rubydex/definition"
36+
require "ruby_lsp/rubydex/reference"
3637

3738
require "ruby-lsp"
3839
require "ruby_lsp/base_server"

lib/ruby_lsp/requests/rename.rb

Lines changed: 63 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def provider
2222
def initialize(global_state, store, document, params)
2323
super()
2424
@global_state = global_state
25+
@graph = global_state.graph #: Rubydex::Graph
2526
@store = store
2627
@document = document
2728
@position = params[:position] #: Hash[Symbol, Integer]
@@ -56,17 +57,14 @@ def perform
5657
name = RubyIndexer::Index.constant_name(target)
5758
return unless name
5859

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
6162

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}"
6465
end
6566

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)
7068

7169
# If the client doesn't support resource operations, such as renaming files, then we can only return the basic
7270
# text changes
@@ -78,12 +76,12 @@ def perform
7876
# renamed and then the URI associated to the text edit no longer exists, causing it to be dropped
7977
document_changes = changes.map do |uri, edits|
8078
Interface::TextDocumentEdit.new(
81-
text_document: Interface::VersionedTextDocumentIdentifier.new(uri: uri, version: nil),
79+
text_document: Interface::OptionalVersionedTextDocumentIdentifier.new(uri: uri, version: nil),
8280
edits: edits,
8381
)
8482
end
8583

86-
collect_file_renames(fully_qualified_name, document_changes)
84+
collect_file_renames(declaration.name, document_changes)
8785
Interface::WorkspaceEdit.new(document_changes: document_changes)
8886
end
8987

@@ -97,80 +95,76 @@ def collect_file_renames(fully_qualified_name, document_changes)
9795
# We also look for an associated test file and rename it too
9896
short_name = fully_qualified_name.split("::").last #: as !nil
9997

100-
@global_state.index[fully_qualified_name]&.each do |entry|
98+
declaration = @graph[fully_qualified_name]
99+
return unless declaration
100+
101+
unless [
102+
Rubydex::Class,
103+
Rubydex::Module,
104+
Rubydex::Constant,
105+
Rubydex::ConstantAlias,
106+
].any? { |type| declaration.is_a?(type) }
107+
return
108+
end
109+
110+
declaration.definitions.each do |definition|
101111
# Do not rename files that are not part of the workspace
102-
uri = entry.uri
112+
uri = URI(definition.location.uri)
103113
file_path = uri.full_path
104114
next unless file_path&.start_with?(@global_state.workspace_path)
105115

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)
116+
file_name = file_from_constant_name(short_name)
117+
next unless "#{file_name}.rb" == File.basename(file_path)
111118

112-
if "#{file_name}.rb" == entry.file_name
113-
new_file_name = file_from_constant_name(
114-
@new_name.split("::").last, #: as !nil
115-
)
119+
new_file_name = file_from_constant_name(
120+
@new_name.split("::").last, #: as !nil
121+
)
116122

117-
new_uri = URI::Generic.from_path(path: File.join(
118-
File.dirname(file_path),
119-
"#{new_file_name}.rb",
120-
)).to_s
123+
new_uri = URI::Generic.from_path(path: File.join(
124+
File.dirname(file_path),
125+
"#{new_file_name}.rb",
126+
)).to_s
121127

122-
document_changes << Interface::RenameFile.new(kind: "rename", old_uri: uri.to_s, new_uri: new_uri)
123-
end
124-
end
128+
document_changes << Interface::RenameFile.new(kind: "rename", old_uri: uri.to_s, new_uri: new_uri)
125129
end
126130
end
127131

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)
132+
#: (Rubydex::Declaration declaration, String name) -> Hash[String, Array[Interface::TextEdit]]
133+
def collect_text_edits(declaration, name)
134+
changes = {} #: Hash[String, Array[Interface::TextEdit]]
135+
short_name = name.split("::").last #: as !nil
136+
new_short_name = @new_name.split("::").last #: as !nil
137+
138+
# Collect edits for definition sites (where the constant is declared)
139+
declaration.definitions.each do |definition|
140+
name_loc = definition.name_location
141+
next unless name_loc
142+
143+
uri_string = name_loc.uri
144+
edits = (changes[uri_string] ||= [])
145+
146+
# The name_location spans the constant name as written in the definition.
147+
# We only replace the unqualified name portion (the last segment).
148+
range = Interface::Range.new(
149+
start: Interface::Position.new(
150+
line: name_loc.end_line,
151+
character: name_loc.end_column - short_name.length,
152+
),
153+
end: Interface::Position.new(line: name_loc.end_line, character: name_loc.end_column),
154+
)
147155

148-
edits = collect_changes(target, document.ast, name, document.uri)
149-
changes[uri] = edits unless edits.empty?
156+
edits << Interface::TextEdit.new(range: range, new_text: new_short_name)
150157
end
151158

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)
159+
# Collect edits for reference sites (where the constant is used)
160+
declaration.references.each do |reference|
161+
ref = reference #: as Rubydex::ConstantReference
162+
uri_string = ref.location.uri
163+
edits = (changes[uri_string] ||= [])
164+
edits << Interface::TextEdit.new(range: ref.to_lsp_range, new_text: new_short_name)
163165
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)
172166

173-
Interface::TextEdit.new(range: range_from_location(location), new_text: new_text)
167+
changes
174168
end
175169

176170
#: (String constant_name) -> String

lib/ruby_lsp/rubydex/reference.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
module Rubydex
5+
class ConstantReference
6+
#: () -> RubyLsp::Interface::Range
7+
def to_lsp_range
8+
loc = location
9+
10+
RubyLsp::Interface::Range.new(
11+
start: RubyLsp::Interface::Position.new(line: loc.start_line, character: loc.start_column),
12+
end: RubyLsp::Interface::Position.new(line: loc.end_line, character: loc.end_column),
13+
)
14+
end
15+
end
16+
end

0 commit comments

Comments
 (0)