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
12 changes: 12 additions & 0 deletions jekyll/index.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,18 @@ Users see a dropdown with all the sources, along with a preview window on the si
Sorry, your browser doesn't support embedded videos. This video demonstrates the go-to-definition feature when multiple definitions are found, showing the dropdown and preview window.
</video>

If a class or module is reopened in many files, jumping to it lists every file where it is defined. If you prefer to
jump directly to the primary definition (the file whose path matches the fully qualified constant name following the
Zeitwerk convention), enable the following setting:

```jsonc
{
"rubyLsp.featuresConfiguration.definition.preferPrimaryDefinition": true
}
```

When no file matches the convention, all definitions are listed as usual.

### Completion

The completion feature provides users with completion candidates when the text they type matches certain indexed components. This helps speed up coding by reducing the need to type out full method names or constants.
Expand Down
4 changes: 4 additions & 0 deletions lib/ruby_lsp/global_state.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ def initialize
enableAll: false,
enableTestCodeLens: true,
}),
definition: RequestConfig.new({
enableAll: false,
preferPrimaryDefinition: false,
}),
} #: Hash[Symbol, RequestConfig]
end

Expand Down
26 changes: 26 additions & 0 deletions lib/ruby_lsp/listeners/definition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,10 @@ def find_in_index(value)
first_entry = entries.first #: as !nil
return if first_entry.private? && first_entry.name != "#{@node_context.fully_qualified_name}::#{value}"

if entries.length > 1 && @global_state.feature_configuration(:definition)&.enabled?(:preferPrimaryDefinition)
entries = select_primary_definitions(entries)
end

entries.each do |entry|
# If the project has Sorbet, then we only want to handle go to definition for constants defined in gems, as an
# additional behavior on top of jumping to RBIs. The only sigil where Sorbet cannot handle constants is typed
Expand All @@ -423,6 +427,28 @@ def find_in_index(value)
)
end
end

#: (Array[RubyIndexer::Entry] entries) -> Array[RubyIndexer::Entry]
def select_primary_definitions(entries)
primary_entries = entries.select do |entry|
entry.is_a?(RubyIndexer::Entry::Namespace) &&
entry.uri.full_path&.end_with?(convention_based_path(entry.name))
end

primary_entries.empty? ? entries : primary_entries
end

#: (String name) -> String
def convention_based_path(name)
parts = name.split("::").map do |part|
part
.gsub(/([A-Z]+)([A-Z][a-z])/, "\\1_\\2")
.gsub(/([a-z\d])([A-Z])/, "\\1_\\2")
.downcase
end

"/#{parts.join("/")}.rb"
end
end
end
end
68 changes: 68 additions & 0 deletions test/requests/definition_expectations_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,74 @@ class Baz
end
end

def test_prefer_primary_definition_when_constant_is_reopened
source = <<~RUBY
RubyLsp::Server
RUBY

with_server(source, stub_no_typechecker: true) do |server, uri|
server.global_state.feature_configuration(:definition)&.merge!(preferPrimaryDefinition: true)

index = server.global_state.index
primary_uri = URI::Generic.from_path(path: "/workspace/lib/ruby_lsp/server.rb")
index.index_single(primary_uri, <<~RUBY)
module RubyLsp
class Server
end
end
RUBY
index.index_single(URI::Generic.from_path(path: "/workspace/lib/ruby_lsp/patches.rb"), <<~RUBY)
module RubyLsp
class Server
end
end
RUBY

server.process_message(
id: 1,
method: "textDocument/definition",
params: { textDocument: { uri: uri }, position: { line: 0, character: 9 } },
)

response = server.pop_response.response
assert_equal(1, response.length)
assert_equal(primary_uri.to_s, response[0].attributes[:targetUri])
end
end

def test_prefer_primary_definition_falls_back_to_all_entries_without_a_convention_match
source = <<~RUBY
RubyLsp::Server
RUBY

with_server(source, stub_no_typechecker: true) do |server, uri|
server.global_state.feature_configuration(:definition)&.merge!(preferPrimaryDefinition: true)

index = server.global_state.index
index.index_single(URI::Generic.from_path(path: "/workspace/lib/one.rb"), <<~RUBY)
module RubyLsp
class Server
end
end
RUBY
index.index_single(URI::Generic.from_path(path: "/workspace/lib/two.rb"), <<~RUBY)
module RubyLsp
class Server
end
end
RUBY

server.process_message(
id: 1,
method: "textDocument/definition",
params: { textDocument: { uri: uri }, position: { line: 0, character: 9 } },
)

response = server.pop_response.response
assert_equal(2, response.length)
end
end

def test_multibyte_character_precision
source = <<~RUBY
module Fほげ
Expand Down
14 changes: 14 additions & 0 deletions vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,20 @@
"default": true
}
}
},
"definition": {
"description": "Customize go to definition features",
"type": "object",
"properties": {
"enableAll": {
"type": "boolean"
},
"preferPrimaryDefinition": {
"description": "When a class or module is reopened in multiple files, jump directly to its primary definition (the file whose path matches the fully qualified constant name) instead of listing every definition",
"type": "boolean",
"default": false
}
}
}
}
},
Expand Down
Loading