Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions jekyll/index.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ Sorry, your browser doesn't support embedded videos. This video demonstrates the
### Go-to-Definition

Go-to-definition allows users to navigate to the target constant or method's definition,
whether they're defined in your project or its dependencies.
whether they're defined in your project or its dependencies. For local variables, it jumps to the parameter or the first
assignment that declares the variable in the current scope.

In VS Code this feature can be triggered by one of the following methods:

Expand Down Expand Up @@ -479,16 +480,24 @@ Rename allows developers to rename all occurrences of the entity under the curso
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 is currently only supported for constants, module names and class names.
Rename is currently supported for constants, module names, class names and local variables (including method and
block parameters).

Local variables are renamed only within the scope that declares them, so a variable with the same name in another method
or block is left untouched. When the variable is used as a shorthand hash value or keyword argument (`{ name: }` or
`foo(name:)`), the key is kept and only the value is renamed (`{ name: new_name }`). Renaming is rejected if the new name
is already used by another local or by a method call in the same scope, since that would change the program's behavior.
Keyword parameters can't be renamed because callers refer to them by name.

<video src="images/rename.mp4" width="100%" controls>
Sorry, your browser doesn't support embedded videos. This video shows renaming a symbol across the project.
</video>

### Find references

The find references request allows users to both see a list of references or jump to reference locations. Note that
only constants are currently supported, but support for methods, instance variables and local variables is planned.
The find references request allows users to both see a list of references or jump to reference locations. It supports
constants, methods, instance variables and local variables. Local variable references are resolved from the current file
only, taking scopes into account, so a local with the same name in another method or block is not included.

<video src="images/references.mp4" width="100%" controls>
Sorry, your browser doesn't support embedded videos. This video demonstrates finding references to a constant.
Expand Down
1 change: 1 addition & 0 deletions lib/ruby_lsp/internal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
require "ruby_lsp/requests/support/annotation"
require "ruby_lsp/requests/support/sorbet"
require "ruby_lsp/requests/support/common"
require "ruby_lsp/requests/support/local_variable_finder"
require "ruby_lsp/requests/support/formatter"
require "ruby_lsp/requests/support/rubocop_runner"
require "ruby_lsp/requests/support/rubocop_formatter"
Expand Down
19 changes: 19 additions & 0 deletions lib/ruby_lsp/requests/definition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,25 @@ def initialize(document, global_state, position, dispatcher, sorbet_level)
char_position, _ = document.find_index_by_position(position)
delegate_request_if_needed!(global_state, document, char_position)

# Sorbet already handles local variables in typed files
local_variable = unless sorbet_level.true_or_higher?
Support::LocalVariableFinder.new(document.ast).variable_at(char_position, document.code_units_cache)
end

if local_variable
declaration = local_variable.first.declaration

if declaration
@response_builder << Interface::Location.new(
uri: document.uri.to_s,
range: Support::LocalVariableFinder.range_for(declaration.location, document.code_units_cache),
)
end

@target = nil #: Prism::Node?
return
end

node_context = RubyDocument.locate(
document.ast,
char_position,
Expand Down
13 changes: 13 additions & 0 deletions lib/ruby_lsp/requests/prepare_rename.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ def initialize(document, position)
def perform
char_position, _ = @document.find_index_by_position(@position)

local_variable = Support::LocalVariableFinder.new(@document.ast).variable_at(
char_position,
@document.code_units_cache,
)

if local_variable
variable, occurrence = local_variable
# Renaming a keyword parameter would break every caller passing it by name
return if variable.occurrences.any? { |o| o.kind == :keyword_parameter }

return Support::LocalVariableFinder.range_for(occurrence.location, @document.code_units_cache)
end

node_context = RubyDocument.locate(
@document.ast,
char_position,
Expand Down
23 changes: 23 additions & 0 deletions lib/ruby_lsp/requests/references.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ def perform
position = @params[:position]
char_position, _ = @document.find_index_by_position(position)

# Local variables can only be referenced in the current document, so there's no need to search the workspace
local_variable = Support::LocalVariableFinder.new(@document.ast).variable_at(
char_position,
@document.code_units_cache,
)
return collect_local_variable_references(local_variable.first) if local_variable

node_context = RubyDocument.locate(
@document.ast,
char_position,
Expand Down Expand Up @@ -81,6 +88,22 @@ def perform

private

#: (Support::LocalVariableFinder::Variable variable) -> Array[Interface::Location]
def collect_local_variable_references(variable)
include_declaration = @params.dig(:context, :includeDeclaration) != false
declaration = variable.declaration
code_units_cache = @document.code_units_cache

variable.occurrences.filter_map do |occurrence|
next if !include_declaration && occurrence.equal?(declaration)

Interface::Location.new(
uri: @document.uri.to_s,
range: Support::LocalVariableFinder.range_for(occurrence.location, code_units_cache),
)
end
end

#: ((Prism::ConstantReadNode | Prism::ConstantPathNode | Prism::ConstantPathTargetNode | Prism::InstanceVariableAndWriteNode | Prism::InstanceVariableOperatorWriteNode | Prism::InstanceVariableOrWriteNode | Prism::InstanceVariableReadNode | Prism::InstanceVariableTargetNode | Prism::InstanceVariableWriteNode | Prism::CallNode | Prism::DefNode) target_node, NodeContext node_context) -> RubyIndexer::ReferenceFinder::Target?
def create_reference_target(target_node, node_context)
case target_node
Expand Down
57 changes: 57 additions & 0 deletions lib/ruby_lsp/requests/rename.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ def initialize(global_state, store, document, params)
def perform
char_position, _ = @document.find_index_by_position(@position)

local_variable_finder = Support::LocalVariableFinder.new(@document.ast)
local_variable = local_variable_finder.variable_at(char_position, @document.code_units_cache)
return rename_local_variable(local_variable_finder, local_variable.first) if local_variable

node_context = RubyDocument.locate(
@document.ast,
char_position,
Expand Down Expand Up @@ -89,6 +93,59 @@ def perform

private

#: (Support::LocalVariableFinder finder, Support::LocalVariableFinder::Variable variable) -> Interface::WorkspaceEdit
def rename_local_variable(finder, variable)
if variable.occurrences.any? { |occurrence| occurrence.kind == :keyword_parameter }
raise InvalidNameError, "Keyword parameters can't be renamed because callers refer to them by name"
end

unless valid_local_variable_name?(@new_name)
raise InvalidNameError, "`#{@new_name}` is not a valid local variable name"
end

old_name = variable.name.to_s
return Interface::WorkspaceEdit.new(changes: {}) if old_name == @new_name

if finder.name_in_use?(variable, @new_name.to_sym)
raise InvalidNameError, "The new name is already in use by a local variable or method call in the same scope"
end

code_units_cache = @document.code_units_cache
edits = variable.occurrences.map do |occurrence|
location = occurrence.location

if occurrence.kind == :shorthand
# `{ name: }` has to become `{ name: new_name }`, otherwise the key would change too
key_location = location.copy(length: location.length + 1)
Interface::TextEdit.new(
range: Support::LocalVariableFinder.range_for(key_location, code_units_cache),
new_text: "#{old_name}: #{@new_name}",
)
else
Interface::TextEdit.new(
range: Support::LocalVariableFinder.range_for(location, code_units_cache),
new_text: @new_name,
)
end
end

Interface::WorkspaceEdit.new(changes: { @document.uri.to_s => edits })
end

#: (String name) -> bool
def valid_local_variable_name?(name)
result = Prism.parse("#{name} = nil")
return false unless result.success?

statements = result.value.statements.body
return false unless statements.length == 1

node = statements.first
return false unless node.is_a?(Prism::LocalVariableWriteNode)

node.name.to_s == name
end

#: (String fully_qualified_name, Array[(Interface::RenameFile | Interface::TextDocumentEdit)] document_changes) -> void
def collect_file_renames(fully_qualified_name, document_changes)
# Check if the declarations of the symbol being renamed match the file name. In case they do, we automatically
Expand Down
Loading
Loading