From cfa126d22036cd894a826a7602a47945f90e055a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lek=C3=AB=20Mula?= Date: Mon, 27 May 2024 17:14:33 +0200 Subject: [PATCH] Implement Go To Type Definition https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_typeDefinition All the necessary pieces were in place, nothing fancy changed, mostly just exposing the functionality for LanguageServer. The only "functionality" added is `ComplexType#namespaces` which also utilizes existing implementation. --- lib/solargraph/complex_type.rb | 7 +++++ lib/solargraph/language_server/host.rb | 10 +++++++ lib/solargraph/language_server/message.rb | 1 + .../language_server/message/initialize.rb | 8 ++++++ .../language_server/message/initialized.rb | 1 + .../language_server/message/text_document.rb | 1 + .../message/text_document/type_definition.rb | 24 ++++++++++++++++ lib/solargraph/library.rb | 16 +++++++++++ lib/solargraph/source_map/clip.rb | 5 ++++ spec/complex_type_spec.rb | 3 ++ spec/fixtures/workspace/lib/something.rb | 2 ++ spec/fixtures/workspace/lib/thing.rb | 4 +-- .../text_document/type_definition_spec.rb | 23 +++++++++++++++ spec/library_spec.rb | 16 +++++++++++ spec/source_map/clip_spec.rb | 28 +++++++++++++++++++ 15 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 lib/solargraph/language_server/message/text_document/type_definition.rb create mode 100644 spec/fixtures/workspace/lib/something.rb create mode 100644 spec/language_server/message/text_document/type_definition_spec.rb diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index db1e1a5b5..2d64e1d1a 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -67,11 +67,18 @@ def [](index) def select &block @items.select &block end + + # @return [String] def namespace # cache this attr for high frequency call @namespace ||= method_missing(:namespace).to_s end + # @return [Array] + def namespaces + @items.map(&:namespace) + end + def method_missing name, *args, &block return if @items.first.nil? return @items.first.send(name, *args, &block) if respond_to_missing?(name) diff --git a/lib/solargraph/language_server/host.rb b/lib/solargraph/language_server/host.rb index b2f6b7e01..3a4fed25d 100644 --- a/lib/solargraph/language_server/host.rb +++ b/lib/solargraph/language_server/host.rb @@ -533,6 +533,15 @@ def definitions_at uri, line, column library.definitions_at(uri_to_file(uri), line, column) end + # @param uri [String] + # @param line [Integer] + # @param column [Integer] + # @return [Array] + def type_definitions_at uri, line, column + library = library_for(uri) + library.type_definitions_at(uri_to_file(uri), line, column) + end + # @param uri [String] # @param line [Integer] # @param column [Integer] @@ -630,6 +639,7 @@ def default_configuration 'hover' => true, 'symbols' => true, 'definitions' => true, + 'typeDefinitions' => true, 'rename' => true, 'references' => true, 'autoformat' => false, diff --git a/lib/solargraph/language_server/message.rb b/lib/solargraph/language_server/message.rb index 6a05c9961..ab74c2eb4 100644 --- a/lib/solargraph/language_server/message.rb +++ b/lib/solargraph/language_server/message.rb @@ -66,6 +66,7 @@ def method_map register 'textDocument/didClose', TextDocument::DidClose register 'textDocument/hover', TextDocument::Hover register 'textDocument/definition', TextDocument::Definition + register 'textDocument/typeDefinition', TextDocument::TypeDefinition register 'textDocument/formatting', TextDocument::Formatting register 'textDocument/onTypeFormatting', TextDocument::OnTypeFormatting register 'textDocument/documentSymbol', TextDocument::DocumentSymbol diff --git a/lib/solargraph/language_server/message/initialize.rb b/lib/solargraph/language_server/message/initialize.rb index 1f634cff8..77f3bfb71 100644 --- a/lib/solargraph/language_server/message/initialize.rb +++ b/lib/solargraph/language_server/message/initialize.rb @@ -34,6 +34,7 @@ def process result[:capabilities].merge! static_document_formatting unless dynamic_registration_for?('textDocument', 'formatting') result[:capabilities].merge! static_document_symbols unless dynamic_registration_for?('textDocument', 'documentSymbol') result[:capabilities].merge! static_definitions unless dynamic_registration_for?('textDocument', 'definition') + result[:capabilities].merge! static_type_definitions unless dynamic_registration_for?('textDocument', 'typeDefinition') result[:capabilities].merge! static_rename unless dynamic_registration_for?('textDocument', 'rename') result[:capabilities].merge! static_references unless dynamic_registration_for?('textDocument', 'references') result[:capabilities].merge! static_workspace_symbols unless dynamic_registration_for?('workspace', 'symbol') @@ -121,6 +122,13 @@ def static_definitions } end + def static_type_definitions + return {} unless host.options['type_definitions'] + { + definitionProvider: true + } + end + def static_rename { renameProvider: {prepareProvider: true} diff --git a/lib/solargraph/language_server/message/initialized.rb b/lib/solargraph/language_server/message/initialized.rb index 64e037fb8..563df6934 100644 --- a/lib/solargraph/language_server/message/initialized.rb +++ b/lib/solargraph/language_server/message/initialized.rb @@ -13,6 +13,7 @@ def process textDocument/formatting textDocument/documentSymbol textDocument/definition + textDocument/typeDefinition textDocument/references textDocument/rename textDocument/prepareRename diff --git a/lib/solargraph/language_server/message/text_document.rb b/lib/solargraph/language_server/message/text_document.rb index 786c88729..124b6a464 100644 --- a/lib/solargraph/language_server/message/text_document.rb +++ b/lib/solargraph/language_server/message/text_document.rb @@ -15,6 +15,7 @@ module TextDocument autoload :DiagnosticsQueue, 'solargraph/language_server/message/text_document/diagnostics_queue' autoload :OnTypeFormatting, 'solargraph/language_server/message/text_document/on_type_formatting' autoload :Definition, 'solargraph/language_server/message/text_document/definition' + autoload :TypeDefinition, 'solargraph/language_server/message/text_document/type_definition' autoload :DocumentSymbol, 'solargraph/language_server/message/text_document/document_symbol' autoload :Formatting, 'solargraph/language_server/message/text_document/formatting' autoload :References, 'solargraph/language_server/message/text_document/references' diff --git a/lib/solargraph/language_server/message/text_document/type_definition.rb b/lib/solargraph/language_server/message/text_document/type_definition.rb new file mode 100644 index 000000000..feb5dfdce --- /dev/null +++ b/lib/solargraph/language_server/message/text_document/type_definition.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Solargraph::LanguageServer::Message::TextDocument + class TypeDefinition < Base + def process + @line = params['position']['line'] + @column = params['position']['character'] + set_result(code_location || []) + end + + private + + def code_location + suggestions = host.type_definitions_at(params['textDocument']['uri'], @line, @column) + return nil if suggestions.empty? + suggestions.reject { |pin| pin.location.nil? || pin.location.filename.nil? }.map do |pin| + { + uri: file_to_uri(pin.location.filename), + range: pin.location.range.to_hash + } + end + end + end +end diff --git a/lib/solargraph/library.rb b/lib/solargraph/library.rb index dff701cb3..178db862e 100644 --- a/lib/solargraph/library.rb +++ b/lib/solargraph/library.rb @@ -199,6 +199,22 @@ def definitions_at filename, line, column handle_file_not_found(filename, e) end + # Get type definition suggestions for the expression at the specified file and + # location. + # + # @param filename [String] The file to analyze + # @param line [Integer] The zero-based line number + # @param column [Integer] The zero-based column number + # @return [Array] + # @todo Take filename/position instead of filename/line/column + def type_definitions_at filename, line, column + position = Position.new(line, column) + cursor = Source::Cursor.new(read(filename), position) + api_map.clip(cursor).types + rescue FileNotFoundError => e + handle_file_not_found filename, e + end + # Get signature suggestions for the method at the specified file and # location. # diff --git a/lib/solargraph/source_map/clip.rb b/lib/solargraph/source_map/clip.rb index 73c0698f3..a7dbc76e0 100644 --- a/lib/solargraph/source_map/clip.rb +++ b/lib/solargraph/source_map/clip.rb @@ -21,6 +21,11 @@ def define result end + # @return [Array] + def types + infer.namespaces.map { |namespace| api_map.get_path_pins(namespace) }.flatten + end + # @return [Completion] def complete return package_completions([]) if !source_map.source.parsed? || cursor.string? diff --git a/spec/complex_type_spec.rb b/spec/complex_type_spec.rb index 63f0d75fe..0c78a9189 100644 --- a/spec/complex_type_spec.rb +++ b/spec/complex_type_spec.rb @@ -59,6 +59,9 @@ expect(types.length).to eq(1) expect(types.first.namespace).to eq('Foo') expect(types.first.scope).to eq(:class) + multiple_types = Solargraph::ComplexType.parse 'Module, Class, String, nil' + expect(multiple_types.length).to eq(4) + expect(multiple_types.namespaces).to eq(['Foo', 'Bar', 'String', 'NilClass']) end it "identifies duck types" do diff --git a/spec/fixtures/workspace/lib/something.rb b/spec/fixtures/workspace/lib/something.rb new file mode 100644 index 000000000..37ec0bde1 --- /dev/null +++ b/spec/fixtures/workspace/lib/something.rb @@ -0,0 +1,2 @@ +class Something +end diff --git a/spec/fixtures/workspace/lib/thing.rb b/spec/fixtures/workspace/lib/thing.rb index 019d7083e..c1fbdab77 100644 --- a/spec/fixtures/workspace/lib/thing.rb +++ b/spec/fixtures/workspace/lib/thing.rb @@ -1,4 +1,4 @@ class Thing - def do_thing - end + # @return [Something] + def do_thing; end end diff --git a/spec/language_server/message/text_document/type_definition_spec.rb b/spec/language_server/message/text_document/type_definition_spec.rb new file mode 100644 index 000000000..2f7ec3668 --- /dev/null +++ b/spec/language_server/message/text_document/type_definition_spec.rb @@ -0,0 +1,23 @@ +describe Solargraph::LanguageServer::Message::TextDocument::TypeDefinition do + it 'finds definitions of methods' do + host = Solargraph::LanguageServer::Host.new + host.prepare('spec/fixtures/workspace') + sleep 0.1 until host.libraries.all?(&:mapped?) + host.catalog + file_uri = Solargraph::LanguageServer::UriHelpers.file_to_uri(File.absolute_path('spec/fixtures/workspace/lib/other.rb')) + something_uri = Solargraph::LanguageServer::UriHelpers.file_to_uri(File.absolute_path('spec/fixtures/workspace/lib/something.rb')) + message = Solargraph::LanguageServer::Message::TextDocument::TypeDefinition.new(host, { + 'params' => { + 'textDocument' => { + 'uri' => file_uri + }, + 'position' => { + 'line' => 4, + 'character' => 10 + } + } + }) + message.process + expect(message.result.first[:uri]).to eq(something_uri) + end +end diff --git a/spec/library_spec.rb b/spec/library_spec.rb index 1f6ec4115..bfc31e114 100644 --- a/spec/library_spec.rb +++ b/spec/library_spec.rb @@ -39,6 +39,22 @@ def bar expect(paths).to include('Foo#bar') end + it "gets type definitions from a file" do + library = Solargraph::Library.new + src = Solargraph::Source.load_string %( + class Bar; end + class Foo + # @return [Bar] + def self.bar + end + end + Foo.bar + ), 'file.rb', 0 + library.attach src + paths = library.type_definitions_at('file.rb', 7, 13).map(&:path) + expect(paths).to include('Bar') + end + it "signifies method arguments" do library = Solargraph::Library.new src = Solargraph::Source.load_string %( diff --git a/spec/source_map/clip_spec.rb b/spec/source_map/clip_spec.rb index da063d9da..a40b70a34 100644 --- a/spec/source_map/clip_spec.rb +++ b/spec/source_map/clip_spec.rb @@ -819,6 +819,34 @@ def bar_method; end expect(clip.complete.pins.map(&:path)).to include('Bar#bar_method') end + it 'finds inferred type definitions' do + source = Solargraph::Source.load_string(%( + class OtherNamespace::MyClass; end + module SomeNamespace + class Foo + # @return [self] + def self.make; end + end + class Bar < Foo + # @return [Class, Bar, OtherNamespace::MyClass] + def foo_method;end + + def bar_method + local_variable = Foo.new + other_variable = local_variable + end + end + end + SomeNamespace::Bar.make.foo_method + ), 'test.rb') + api_map = Solargraph::ApiMap.new + api_map.map source + clip = api_map.clip_at('test.rb', [13, 33]) + expect(clip.types.map(&:path)).to eq(['SomeNamespace::Foo']) # other_variable + clip = api_map.clip_at('test.rb', [17, 33]) + expect(clip.types.map(&:path)).to eq(['SomeNamespace::Foo', 'SomeNamespace::Bar', 'OtherNamespace::MyClass']) + end + it 'infers Hash value types' do source = Solargraph::Source.load_string(%( # @type [Hash{String => File}]