Skip to content

Commit

Permalink
Add rename support for constants
Browse files Browse the repository at this point in the history
  • Loading branch information
vinistock committed Sep 25, 2024
1 parent b8d3954 commit 16b741b
Show file tree
Hide file tree
Showing 10 changed files with 485 additions and 0 deletions.
Binary file added jekyll/images/rename.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions jekyll/index.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Want to discuss Ruby developer experience? Consider joining the public
- [Show syntax tree](#show-syntax-tree)
- [ERB support](#erb-support)
- [Guessed types](#guessed-types)
- [Rename](#rename)
- [VS Code only features](#vs-code-features)
- [Dependencies view](#dependencies-view)
- [Rails generator integrations](#rails-generator-integrations)
Expand Down Expand Up @@ -403,6 +404,13 @@ end
# randomly
user.a
```
### Rename

Rename allows developers to rename all occurrences of the entity under the cursor across the entire project. In VS Code
renaming can be triggered by right clicking the entity to rename or by pressing F2 on it.

![Rename demo](images/rename.gif)

## VS Code features

The following features are all custom made for VS Code.
Expand Down
264 changes: 264 additions & 0 deletions lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
# typed: strict
# frozen_string_literal: true

module RubyIndexer
class ReferenceFinder
extend T::Sig

class Reference
extend T::Sig

sig { returns(String) }
attr_reader :name

sig { returns(Prism::Location) }
attr_reader :location

sig { params(name: String, location: Prism::Location).void }
def initialize(name, location)
@name = name
@location = location
end
end

sig { returns(T::Array[Reference]) }
attr_reader :references

sig do
params(
fully_qualified_name: String,
index: RubyIndexer::Index,
dispatcher: Prism::Dispatcher,
parse_result: Prism::ParseResult,
).void
end
def initialize(fully_qualified_name, index, dispatcher, parse_result)
@fully_qualified_name = fully_qualified_name
@index = index
@inside_def = T.let(false, T::Boolean)
@stack = T.let([], T::Array[String])
@references = T.let([], T::Array[Reference])

dispatcher.register(
self,
:on_class_node_enter,
:on_class_node_leave,
:on_module_node_enter,
:on_module_node_leave,
:on_singleton_class_node_enter,
:on_singleton_class_node_leave,
:on_def_node_enter,
:on_def_node_leave,
:on_multi_write_node_enter,
:on_constant_path_write_node_enter,
:on_constant_path_or_write_node_enter,
:on_constant_path_operator_write_node_enter,
:on_constant_path_and_write_node_enter,
:on_constant_or_write_node_enter,
:on_constant_path_node_enter,
:on_constant_read_node_enter,
:on_constant_write_node_enter,
:on_constant_or_write_node_enter,
:on_constant_and_write_node_enter,
:on_constant_operator_write_node_enter,
)
end

sig { params(node: Prism::ClassNode).void }
def on_class_node_enter(node)
constant_path = node.constant_path
name = constant_path.slice
nesting = actual_nesting(name)

if nesting.join("::") == @fully_qualified_name
@references << Reference.new(name, constant_path.location)
end

@stack << name
end

sig { params(node: Prism::ClassNode).void }
def on_class_node_leave(node)
@stack.pop
end

sig { params(node: Prism::ModuleNode).void }
def on_module_node_enter(node)
constant_path = node.constant_path
name = constant_path.slice
nesting = actual_nesting(name)

if nesting.join("::") == @fully_qualified_name
@references << Reference.new(name, constant_path.location)
end

@stack << name
end

sig { params(node: Prism::ModuleNode).void }
def on_module_node_leave(node)
@stack.pop
end

sig { params(node: Prism::SingletonClassNode).void }
def on_singleton_class_node_enter(node)
expression = node.expression
return unless expression.is_a?(Prism::SelfNode)

@stack << "<Class:#{@stack.last}>"
end

sig { params(node: Prism::SingletonClassNode).void }
def on_singleton_class_node_leave(node)
@stack.pop
end

sig { params(node: Prism::ConstantPathNode).void }
def on_constant_path_node_enter(node)
name = constant_name(node)
return unless name

collect_constant_references(name, node.location)
end

sig { params(node: Prism::ConstantReadNode).void }
def on_constant_read_node_enter(node)
name = constant_name(node)
return unless name

collect_constant_references(name, node.location)
end

sig { params(node: Prism::MultiWriteNode).void }
def on_multi_write_node_enter(node)
[*node.lefts, *node.rest, *node.rights].each do |target|
case target
when Prism::ConstantTargetNode, Prism::ConstantPathTargetNode
collect_constant_references(target.name.to_s, target.location)
end
end
end

sig { params(node: Prism::ConstantPathWriteNode).void }
def on_constant_path_write_node_enter(node)
target = node.target
return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)

name = constant_name(target)
return unless name

collect_constant_references(name, target.location)
end

sig { params(node: Prism::ConstantPathOrWriteNode).void }
def on_constant_path_or_write_node_enter(node)
target = node.target
return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)

name = constant_name(target)
return unless name

collect_constant_references(name, target.location)
end

sig { params(node: Prism::ConstantPathOperatorWriteNode).void }
def on_constant_path_operator_write_node_enter(node)
target = node.target
return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)

name = constant_name(target)
return unless name

collect_constant_references(name, target.location)
end

sig { params(node: Prism::ConstantPathAndWriteNode).void }
def on_constant_path_and_write_node_enter(node)
target = node.target
return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)

name = constant_name(target)
return unless name

collect_constant_references(name, target.location)
end

sig { params(node: Prism::ConstantWriteNode).void }
def on_constant_write_node_enter(node)
collect_constant_references(node.name.to_s, node.name_loc)
end

sig { params(node: Prism::ConstantOrWriteNode).void }
def on_constant_or_write_node_enter(node)
collect_constant_references(node.name.to_s, node.name_loc)
end

sig { params(node: Prism::ConstantAndWriteNode).void }
def on_constant_and_write_node_enter(node)
collect_constant_references(node.name.to_s, node.name_loc)
end

sig { params(node: Prism::ConstantOperatorWriteNode).void }
def on_constant_operator_write_node_enter(node)
collect_constant_references(node.name.to_s, node.name_loc)
end

sig { params(node: Prism::DefNode).void }
def on_def_node_enter(node)
if node.receiver.is_a?(Prism::SelfNode)
@stack << "<Class:#{@stack.last}>"
end
end

sig { params(node: Prism::DefNode).void }
def on_def_node_leave(node)
if node.receiver.is_a?(Prism::SelfNode)
@stack.pop
end
end

private

sig { params(name: String).returns(T::Array[String]) }
def actual_nesting(name)
nesting = @stack + [name]
corrected_nesting = []

nesting.reverse_each do |name|
corrected_nesting.prepend(name.delete_prefix("::"))

break if name.start_with?("::")
end

corrected_nesting
end

sig { params(name: String, location: Prism::Location).void }
def collect_constant_references(name, location)
entries = @index.resolve(name, @stack)
return unless entries

entries.each do |entry|
next unless entry.name == @fully_qualified_name

@references << Reference.new(name, location)
end
end

sig do
params(
node: T.any(
Prism::ConstantPathNode,
Prism::ConstantReadNode,
Prism::ConstantPathTargetNode,
),
).returns(T.nilable(String))
end
def constant_name(node)
node.full_name
rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError,
Prism::ConstantPathNode::MissingNodesInConstantPathError
nil
end
end
end
1 change: 1 addition & 0 deletions lib/ruby_indexer/ruby_indexer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

require "ruby_indexer/lib/ruby_indexer/indexable_path"
require "ruby_indexer/lib/ruby_indexer/declaration_listener"
require "ruby_indexer/lib/ruby_indexer/reference_finder"
require "ruby_indexer/lib/ruby_indexer/enhancement"
require "ruby_indexer/lib/ruby_indexer/index"
require "ruby_indexer/lib/ruby_indexer/entry"
Expand Down
43 changes: 43 additions & 0 deletions lib/ruby_indexer/test/reference_finder_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# typed: true
# frozen_string_literal: true

require "test_helper"

module RubyIndexer
class ReferenceFinderTest < Minitest::Test
def test_finds_constant_references
refs = find_references("Foo::Bar", <<~RUBY)
module Foo
class Bar
end
Bar
end
Foo::Bar
RUBY

assert_equal("Bar", refs[0].name)
assert_equal(2, refs[0].location.start_line)

assert_equal("Bar", refs[1].name)
assert_equal(5, refs[1].location.start_line)

assert_equal("Foo::Bar", refs[2].name)
assert_equal(8, refs[2].location.start_line)
end

private

def find_references(fully_qualified_name, source)
file_path = "/fake.rb"
index = Index.new
index.index_single(IndexablePath.new(nil, file_path), source)
parse_result = Prism.parse(source)
dispatcher = Prism::Dispatcher.new
finder = ReferenceFinder.new(fully_qualified_name, index, dispatcher, parse_result)
dispatcher.visit(parse_result.value)
finder.references.uniq(&:location)
end
end
end
1 change: 1 addition & 0 deletions lib/ruby_lsp/internal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,4 @@
require "ruby_lsp/requests/signature_help"
require "ruby_lsp/requests/type_hierarchy_supertypes"
require "ruby_lsp/requests/workspace_symbol"
require "ruby_lsp/requests/rename"
Loading

0 comments on commit 16b741b

Please sign in to comment.