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
52 changes: 15 additions & 37 deletions lib/ruby_lsp/requests/workspace_symbol.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,54 +12,32 @@ class WorkspaceSymbol < Request
#: (GlobalState global_state, String? query) -> void
def initialize(global_state, query)
super()
@global_state = global_state
@query = query
@index = global_state.index #: RubyIndexer::Index
@graph = global_state.graph #: Rubydex::Graph
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we don't need @global_state anymore, does it make sense to just pass graph in?

end

# @override
#: -> Array[Interface::WorkspaceSymbol]
def perform
fuzzy_search.filter_map do |entry|
kind = kind_for_entry(entry)
loc = entry.location
response = []

# We use the namespace as the container name, but we also use the full name as the regular name. The reason we
# do this is to allow people to search for fully qualified names (e.g.: `Foo::Bar`). If we only included the
# short name `Bar`, then searching for `Foo::Bar` would not return any results
*container, _short_name = entry.name.split("::")
@graph.search(@query || "").each do |declaration|
name = declaration.name

Interface::WorkspaceSymbol.new(
name: entry.name,
container_name: container.join("::"),
kind: kind,
location: Interface::Location.new(
uri: entry.uri.to_s,
range: Interface::Range.new(
start: Interface::Position.new(line: loc.start_line - 1, character: loc.start_column),
end: Interface::Position.new(line: loc.end_line - 1, character: loc.end_column),
),
),
)
end
end

private
declaration.definitions.each do |definition|
location = definition.location
uri = URI(location.uri)
file_path = uri.full_path
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not blocking, but does it make sense to make this Definition#full_path?


#: -> Array[RubyIndexer::Entry]
def fuzzy_search
@index.fuzzy_search(@query) do |entry|
file_path = entry.uri.full_path
# We only show symbols declared in the workspace
in_dependencies = file_path && !not_in_dependencies?(file_path)
next if in_dependencies

# We only show symbols declared in the workspace
in_dependencies = file_path && !not_in_dependencies?(file_path)
next if in_dependencies

# We should never show private symbols when searching the entire workspace
next if entry.private?

true
response << definition.to_lsp_workspace_symbol(name)
end
end

response
end
end
end
Expand Down
135 changes: 135 additions & 0 deletions lib/ruby_lsp/rubydex/definition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,30 @@
# frozen_string_literal: true

module Rubydex
# @abstract
class Definition
# @abstract
#: () -> Integer
def to_lsp_kind
raise RubyLsp::AbstractMethodInvokedError
end

#: (String name) -> RubyLsp::Interface::WorkspaceSymbol
def to_lsp_workspace_symbol(name)
# We use the namespace as the container name, but we also use the full name as the regular name. The reason we do
# this is to allow people to search for fully qualified names (e.g.: `Foo::Bar`). If we only included the short
# name `Bar`, then searching for `Foo::Bar` would not return any results
*container, _short_name = name.split("::")
container_name = container.join("::")

RubyLsp::Interface::WorkspaceSymbol.new(
name: name,
container_name: container_name,
kind: to_lsp_kind,
location: to_lsp_selection_location,
)
end

#: () -> RubyLsp::Interface::Range
def to_lsp_selection_range
loc = location
Expand Down Expand Up @@ -51,4 +74,116 @@ def to_lsp_name_location
)
end
end

class ClassDefinition
# @override
#: () -> Integer
def to_lsp_kind
RubyLsp::Constant::SymbolKind::CLASS
end
end

class ModuleDefinition
# @override
#: () -> Integer
def to_lsp_kind
RubyLsp::Constant::SymbolKind::NAMESPACE
end
end

class SingletonClassDefinition
# @override
#: () -> Integer
def to_lsp_kind
RubyLsp::Constant::SymbolKind::CLASS
end
end

class ConstantDefinition
# @override
#: () -> Integer
def to_lsp_kind
RubyLsp::Constant::SymbolKind::CONSTANT
end
end

class ConstantAliasDefinition
# @override
#: () -> Integer
def to_lsp_kind
RubyLsp::Constant::SymbolKind::CONSTANT
end
end

class MethodDefinition
# @override
#: () -> Integer
def to_lsp_kind
name == "initialize()" ? RubyLsp::Constant::SymbolKind::CONSTRUCTOR : RubyLsp::Constant::SymbolKind::METHOD
end
end

class MethodAliasDefinition
# @override
#: () -> Integer
def to_lsp_kind
RubyLsp::Constant::SymbolKind::METHOD
end
end

class AttrReaderDefinition
# @override
#: () -> Integer
def to_lsp_kind
RubyLsp::Constant::SymbolKind::PROPERTY
end
end

class AttrWriterDefinition
# @override
#: () -> Integer
def to_lsp_kind
RubyLsp::Constant::SymbolKind::PROPERTY
end
end

class AttrAccessorDefinition
# @override
#: () -> Integer
def to_lsp_kind
RubyLsp::Constant::SymbolKind::PROPERTY
end
end

class InstanceVariableDefinition
# @override
#: () -> Integer
def to_lsp_kind
RubyLsp::Constant::SymbolKind::FIELD
end
end

class ClassVariableDefinition
# @override
#: () -> Integer
def to_lsp_kind
RubyLsp::Constant::SymbolKind::FIELD
end
end

class GlobalVariableDefinition
# @override
#: () -> Integer
def to_lsp_kind
RubyLsp::Constant::SymbolKind::VARIABLE
end
end

class GlobalVariableAliasDefinition
# @override
#: () -> Integer
def to_lsp_kind
RubyLsp::Constant::SymbolKind::VARIABLE
end
end
end
83 changes: 44 additions & 39 deletions test/requests/workspace_symbol_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@ class WorkspaceSymbolTest < Minitest::Test
def setup
@global_state = RubyLsp::GlobalState.new
@global_state.stubs(:has_type_checker).returns(false)
@index = @global_state.index
@graph = @global_state.graph
end

def test_returns_index_entries_based_on_query
@index.index_single(URI::Generic.from_path(path: "/fake.rb"), <<~RUBY)
index_source(<<~RUBY)
class Foo; end
module Bar; end

CONSTANT = 1
RUBY

result = RubyLsp::Requests::WorkspaceSymbol.new(@global_state, "Foo").perform.first
result = RubyLsp::Requests::WorkspaceSymbol.new(@global_state, "Fo").perform.first
assert_equal("Foo", result&.name)
assert_equal(RubyLsp::Constant::SymbolKind::CLASS, result&.kind)

Expand All @@ -31,29 +31,8 @@ module Bar; end
assert_equal(RubyLsp::Constant::SymbolKind::CONSTANT, result&.kind)
end

def test_fuzzy_matches_symbols
@index.index_single(URI::Generic.from_path(path: "/fake.rb"), <<~RUBY)
class Foo; end
module Bar; end

CONSTANT = 1
RUBY

result = RubyLsp::Requests::WorkspaceSymbol.new(@global_state, "Floo").perform.first
assert_equal("Foo", result&.name)
assert_equal(RubyLsp::Constant::SymbolKind::CLASS, result&.kind)

result = RubyLsp::Requests::WorkspaceSymbol.new(@global_state, "Bear").perform.first
assert_equal("Bar", result&.name)
assert_equal(RubyLsp::Constant::SymbolKind::NAMESPACE, result&.kind)

result = RubyLsp::Requests::WorkspaceSymbol.new(@global_state, "CONF").perform.first
assert_equal("CONSTANT", result&.name)
assert_equal(RubyLsp::Constant::SymbolKind::CONSTANT, result&.kind)
end

def test_symbols_include_container_name
@index.index_single(URI::Generic.from_path(path: "/fake.rb"), <<~RUBY)
index_source(<<~RUBY)
module Foo
class Bar; end
end
Expand All @@ -66,27 +45,44 @@ class Bar; end
end

def test_does_not_include_symbols_from_dependencies
@index.index_file(URI::Generic.from_path(path: "#{RbConfig::CONFIG["rubylibdir"]}/pathname.rb"))
@graph.index_all(["#{RbConfig::CONFIG["rubylibdir"]}/pathname.rb"])
@graph.resolve

result = RubyLsp::Requests::WorkspaceSymbol.new(@global_state, "Pathname").perform
assert_empty(result)
end

def test_does_not_include_private_constants
@index.index_single(URI::Generic.from_path(path: "/fake.rb"), <<~RUBY)
def test_includes_private_and_protected_symbols
index_source(<<~RUBY)
class Foo
CONSTANT = 1
private_constant(:CONSTANT)

private

def secret; end

protected

def internal; end
end
RUBY

result = RubyLsp::Requests::WorkspaceSymbol.new(@global_state, "Foo::CONSTANT").perform
assert_equal(1, result.length)
assert_equal("Foo", result.first&.name)
result = RubyLsp::Requests::WorkspaceSymbol.new(@global_state, "Foo::CONSTANT").perform.first
assert_equal("Foo::CONSTANT", result&.name)
assert_equal(RubyLsp::Constant::SymbolKind::CONSTANT, result&.kind)

result = RubyLsp::Requests::WorkspaceSymbol.new(@global_state, "Foo#secret").perform.first
assert_equal("Foo#secret()", result&.name)
assert_equal(RubyLsp::Constant::SymbolKind::METHOD, result&.kind)

result = RubyLsp::Requests::WorkspaceSymbol.new(@global_state, "Foo#internal").perform.first
assert_equal("Foo#internal()", result&.name)
assert_equal(RubyLsp::Constant::SymbolKind::METHOD, result&.kind)
end

def test_returns_method_symbols
@index.index_single(URI::Generic.from_path(path: "/fake.rb"), <<~RUBY)
index_source(<<~RUBY)
class Foo
attr_reader :baz

Expand All @@ -95,26 +91,35 @@ def bar; end
end
RUBY

result = RubyLsp::Requests::WorkspaceSymbol.new(@global_state, "bar").perform.first
assert_equal("bar", result&.name)
result = RubyLsp::Requests::WorkspaceSymbol.new(@global_state, "Foo#bar").perform.first
assert_equal("Foo#bar()", result&.name)
assert_equal(RubyLsp::Constant::SymbolKind::METHOD, result&.kind)

result = RubyLsp::Requests::WorkspaceSymbol.new(@global_state, "initialize").perform.first
assert_equal("initialize", result&.name)
result = RubyLsp::Requests::WorkspaceSymbol.new(@global_state, "Foo#initialize").perform.first
assert_equal("Foo#initialize()", result&.name)
assert_equal(RubyLsp::Constant::SymbolKind::CONSTRUCTOR, result&.kind)

result = RubyLsp::Requests::WorkspaceSymbol.new(@global_state, "baz").perform.first
assert_equal("baz", result&.name)
result = RubyLsp::Requests::WorkspaceSymbol.new(@global_state, "Foo#baz").perform.first
assert_equal("Foo#baz()", result&.name)
assert_equal(RubyLsp::Constant::SymbolKind::PROPERTY, result&.kind)
end

def test_returns_symbols_from_unsaved_files
@index.index_single(URI("untitled:Untitled-1"), <<~RUBY)
@graph.index_source("untitled:Untitled-1", <<~RUBY, "ruby")
class Foo; end
RUBY
@graph.resolve

result = RubyLsp::Requests::WorkspaceSymbol.new(@global_state, "Foo").perform.first
assert_equal("Foo", result&.name)
assert_equal(RubyLsp::Constant::SymbolKind::CLASS, result&.kind)
end

private

#: (String, ?uri: String) -> void
def index_source(source, uri: URI::Generic.from_path(path: "/fake.rb").to_s)
@graph.index_source(uri, source, "ruby")
@graph.resolve
end
end
Loading