From 23fc40e3338c35f9bf1e0c424f6aa68cadc69cad Mon Sep 17 00:00:00 2001 From: Vinicius Stock Date: Thu, 19 Mar 2026 12:43:39 -0400 Subject: [PATCH] Migrate workspace symbol to use Rubydex --- lib/ruby_lsp/requests/workspace_symbol.rb | 52 +++------ lib/ruby_lsp/rubydex/definition.rb | 135 ++++++++++++++++++++++ test/requests/workspace_symbol_test.rb | 83 ++++++------- 3 files changed, 194 insertions(+), 76 deletions(-) diff --git a/lib/ruby_lsp/requests/workspace_symbol.rb b/lib/ruby_lsp/requests/workspace_symbol.rb index f41be04957..b4a81cdb68 100644 --- a/lib/ruby_lsp/requests/workspace_symbol.rb +++ b/lib/ruby_lsp/requests/workspace_symbol.rb @@ -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 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 - #: -> 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 diff --git a/lib/ruby_lsp/rubydex/definition.rb b/lib/ruby_lsp/rubydex/definition.rb index 3b89b29c2e..7306180fe5 100644 --- a/lib/ruby_lsp/rubydex/definition.rb +++ b/lib/ruby_lsp/rubydex/definition.rb @@ -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 @@ -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 diff --git a/test/requests/workspace_symbol_test.rb b/test/requests/workspace_symbol_test.rb index 0f683abb94..ff29f139ad 100644 --- a/test/requests/workspace_symbol_test.rb +++ b/test/requests/workspace_symbol_test.rb @@ -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) @@ -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 @@ -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 @@ -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