Skip to content

Commit c6c2a82

Browse files
authored
Merge pull request #4033 from Shopify/vs_migrate_rename
Migrate rename to use Rubydex
2 parents 8f7775f + 5da44df commit c6c2a82

4 files changed

Lines changed: 235 additions & 156 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: 64 additions & 72 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,99 +76,93 @@ 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, document_changes)
8785
Interface::WorkspaceEdit.new(document_changes: document_changes)
8886
end
8987

9088
private
9189

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)
9492
# Check if the declarations of the symbol being renamed match the file name. In case they do, we automatically
9593
# rename the files for the user.
9694
#
9795
# We also look for an associated test file and rename it too
98-
short_name = fully_qualified_name.split("::").last #: as !nil
9996

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|
101109
# Do not rename files that are not part of the workspace
102-
uri = entry.uri
110+
uri = URI(definition.location.uri)
103111
file_path = uri.full_path
104112
next unless file_path&.start_with?(@global_state.workspace_path)
105113

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

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+
)
116120

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
121125

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)
125127
end
126128
end
127129

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+
)
147153

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)
150155
end
151156

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)
163163
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)
172164

173-
Interface::TextEdit.new(range: range_from_location(location), new_text: new_text)
165+
changes
174166
end
175167

176168
#: (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)