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 Oct 2, 2024
1 parent ae70ebc commit 21f56af
Show file tree
Hide file tree
Showing 12 changed files with 694 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.
9 changes: 9 additions & 0 deletions jekyll/index.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,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 symbol](#rename-symbol)
- [VS Code only features](#vs-code-features)
- [Dependencies view](#dependencies-view)
- [Rails generator integrations](#rails-generator-integrations)
Expand Down Expand Up @@ -409,6 +410,14 @@ end
# randomly
user.a
```
### Rename symbol

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. You can also preview the
edits that will be applied by pressing CTRL/CMD + Enter after typing the desired new name.

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

## VS Code features

The following features are all custom made for VS Code.
Expand Down
262 changes: 262 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,262 @@
# 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,
).void
end
def initialize(fully_qualified_name, index, dispatcher)
@fully_qualified_name = fully_qualified_name
@index = index
@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
86 changes: 86 additions & 0 deletions lib/ruby_indexer/test/reference_finder_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# 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

def test_finds_constant_references_inside_singleton_contexts
refs = find_references("Foo::<Class:Foo>::Bar", <<~RUBY)
class Foo
class << self
class Bar
end
Bar
end
end
RUBY

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

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

def test_finds_top_level_constant_references
refs = find_references("Bar", <<~RUBY)
class Bar
end
class Foo
::Bar
class << self
::Bar
end
end
RUBY

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

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

assert_equal("::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)
dispatcher.visit(parse_result.value)
finder.references.uniq(&:location)
end
end
end
Loading

0 comments on commit 21f56af

Please sign in to comment.