ruby-lsp 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 62174da68ba92c0af61878221168a02398c9b3df2867b63c063850b1787c7104
4
- data.tar.gz: dc3d2e17e3fe2dcbb6578902eb3b672babc72d0f10511c021db767cfde709b0b
3
+ metadata.gz: 9a8db1decbd8a8d3a05226ececbd13ba54aa2a0b5e727fd9a752ccda5b52322d
4
+ data.tar.gz: 4f49ca2d80313dcd41e0410a1583ddc3f0c913305b4c3347611181a752561265
5
5
  SHA512:
6
- metadata.gz: c8295ce6739f9452dd20cab22ebb19fb099040f4782afa4dfd63b423b08ad73eedeb1fccc33113dcd7b27d38745e7112a6d272ea28652e00102a870c18c51b96
7
- data.tar.gz: 1e1a2a681dcdf97bbb1f667211111b89ca274c1d89745bcd3cf8511fbc3d41416d0bb5a45f2fcaf85feb8b0469a5441b5500148136f849657d263531ffe5ff8a
6
+ metadata.gz: 510bd7ef4da254c79da9af3437403ffe70a054718e9b0a33efc52b1bb49f93cf9c951729e6528ad4cbcf7d383fa7904835f8e0496482611c3891c3ed12ba784c
7
+ data.tar.gz: 2df5df95106e85a37c834c4ff77a5a19609e1946326ca86828292abb4440787b0eb8f38c9fd8f6f3cd2eb5460e8c27d0c41bb1c025d317f01502ab21afad4161
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.3
1
+ 0.3.4
@@ -24,6 +24,7 @@ module RubyLsp
24
24
  @syntax_error_edits = T.let([], T::Array[EditShape])
25
25
  @source = T.let(source, String)
26
26
  @parsable_source = T.let(source.dup, String)
27
+ @unparsed_edits = T.let([], T::Array[EditShape])
27
28
  @tree = T.let(SyntaxTree.parse(@source), T.nilable(SyntaxTree::Node))
28
29
  rescue SyntaxTree::Parser::ParseError
29
30
  # Do not raise if we failed to parse
@@ -55,13 +56,21 @@ module RubyLsp
55
56
  # Apply the edits on the real source
56
57
  edits.each { |edit| apply_edit(@source, edit[:range], edit[:text]) }
57
58
 
59
+ @unparsed_edits.concat(edits)
58
60
  @cache.clear
61
+ end
62
+
63
+ sig { void }
64
+ def parse
65
+ return if @unparsed_edits.empty?
66
+
59
67
  @tree = SyntaxTree.parse(@source)
60
68
  @syntax_error_edits.clear
69
+ @unparsed_edits.clear
61
70
  @parsable_source = @source.dup
62
- nil
63
71
  rescue SyntaxTree::Parser::ParseError
64
- update_parsable_source(edits)
72
+ @syntax_error_edits = @unparsed_edits
73
+ update_parsable_source(@unparsed_edits)
65
74
  end
66
75
 
67
76
  sig { returns(T::Boolean) }
@@ -81,7 +90,6 @@ module RubyLsp
81
90
  # If the new edits caused a syntax error, make all edits blank spaces and line breaks to adjust the line and
82
91
  # column numbers. This is attempt to make the document parsable while partial edits are being applied
83
92
  edits.each do |edit|
84
- @syntax_error_edits << edit
85
93
  next if edit[:text].empty? # skip deletions, since they may have caused the syntax error
86
94
 
87
95
  apply_edit(@parsable_source, edit[:range], edit[:text].gsub(/[^\r\n]/, " "))
@@ -14,6 +14,10 @@ module RubyLsp
14
14
  def initialize(document)
15
15
  @document = document
16
16
 
17
+ # Parsing the document here means we're taking a lazy approach by only doing it when the first feature request
18
+ # is received by the server. This happens because {Document#parse} remembers if there are new edits to be parsed
19
+ @document.parse
20
+
17
21
  super()
18
22
  end
19
23
 
@@ -30,6 +34,51 @@ module RubyLsp
30
34
  end: LanguageServer::Protocol::Interface::Position.new(line: loc.end_line - 1, character: loc.end_column),
31
35
  )
32
36
  end
37
+
38
+ sig { params(node: SyntaxTree::ConstPathRef).returns(String) }
39
+ def full_constant_name(node)
40
+ name = +node.constant.value
41
+ constant = T.let(node, SyntaxTree::Node)
42
+
43
+ while constant.is_a?(SyntaxTree::ConstPathRef)
44
+ constant = constant.parent
45
+
46
+ case constant
47
+ when SyntaxTree::ConstPathRef
48
+ name.prepend("#{constant.constant.value}::")
49
+ when SyntaxTree::VarRef
50
+ name.prepend("#{constant.value.value}::")
51
+ end
52
+ end
53
+
54
+ name
55
+ end
56
+
57
+ sig do
58
+ params(
59
+ parent: SyntaxTree::Node,
60
+ target_nodes: T::Array[T.class_of(SyntaxTree::Node)],
61
+ position: Integer,
62
+ ).returns(T::Array[SyntaxTree::Node])
63
+ end
64
+ def locate_node_and_parent(parent, target_nodes, position)
65
+ matched = parent.child_nodes.compact.bsearch do |child|
66
+ if (child.location.start_char...child.location.end_char).cover?(position)
67
+ 0
68
+ else
69
+ position <=> child.location.start_char
70
+ end
71
+ end
72
+
73
+ case matched
74
+ when *target_nodes
75
+ [matched, parent]
76
+ when SyntaxTree::Node
77
+ locate_node_and_parent(matched, target_nodes, position)
78
+ else
79
+ []
80
+ end
81
+ end
33
82
  end
34
83
  end
35
84
  end
@@ -37,9 +37,7 @@ module RubyLsp
37
37
  sig { override.returns(T.all(T::Array[LanguageServer::Protocol::Interface::DocumentHighlight], Object)) }
38
38
  def run
39
39
  # no @target means the target is not highlightable
40
- return [] unless @target
41
-
42
- visit(@document.tree)
40
+ visit(@document.tree) if @document.parsed? && @target
43
41
  @highlights
44
42
  end
45
43
 
@@ -55,6 +53,14 @@ module RubyLsp
55
53
 
56
54
  private
57
55
 
56
+ DIRECT_HIGHLIGHTS = T.let([
57
+ SyntaxTree::GVar,
58
+ SyntaxTree::IVar,
59
+ SyntaxTree::Const,
60
+ SyntaxTree::CVar,
61
+ SyntaxTree::VarField,
62
+ ], T::Array[T.class_of(SyntaxTree::Node)])
63
+
58
64
  sig do
59
65
  params(
60
66
  node: SyntaxTree::Node,
@@ -62,27 +68,16 @@ module RubyLsp
62
68
  ).returns(T.nilable(Support::HighlightTarget))
63
69
  end
64
70
  def find(node, position)
65
- matched =
66
- node.child_nodes.compact.bsearch do |child|
67
- if (child.location.start_char...child.location.end_char).cover?(position)
68
- 0
69
- else
70
- position <=> child.location.start_char
71
- end
72
- end
71
+ matched, parent = locate_node_and_parent(node, DIRECT_HIGHLIGHTS + [SyntaxTree::Ident], position)
72
+
73
+ return unless matched && parent
73
74
 
74
75
  case matched
75
- when SyntaxTree::GVar,
76
- SyntaxTree::IVar,
77
- SyntaxTree::Const,
78
- SyntaxTree::CVar,
79
- SyntaxTree::VarField
76
+ when *DIRECT_HIGHLIGHTS
80
77
  Support::HighlightTarget.new(matched)
81
78
  when SyntaxTree::Ident
82
- relevant_node = node.is_a?(SyntaxTree::Params) ? matched : node
79
+ relevant_node = parent.is_a?(SyntaxTree::Params) ? matched : parent
83
80
  Support::HighlightTarget.new(relevant_node)
84
- when SyntaxTree::Node
85
- find(matched, position)
86
81
  end
87
82
  end
88
83
 
@@ -78,7 +78,7 @@ module RubyLsp
78
78
 
79
79
  sig { override.returns(T.all(T::Array[LanguageServer::Protocol::Interface::DocumentLink], Object)) }
80
80
  def run
81
- visit(@document.tree)
81
+ visit(@document.tree) if @document.parsed?
82
82
  @links
83
83
  end
84
84
 
@@ -85,7 +85,7 @@ module RubyLsp
85
85
 
86
86
  sig { override.returns(T.all(T::Array[LanguageServer::Protocol::Interface::DocumentSymbol], Object)) }
87
87
  def run
88
- visit(@document.tree)
88
+ visit(@document.tree) if @document.parsed?
89
89
  @root.children
90
90
  end
91
91
 
@@ -66,8 +66,11 @@ module RubyLsp
66
66
 
67
67
  sig { override.returns(T.all(T::Array[LanguageServer::Protocol::Interface::FoldingRange], Object)) }
68
68
  def run
69
- visit(@document.tree)
70
- emit_partial_range
69
+ if @document.parsed?
70
+ visit(@document.tree)
71
+ emit_partial_range
72
+ end
73
+
71
74
  @ranges
72
75
  end
73
76
 
@@ -0,0 +1,74 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "ruby_lsp/requests/support/rails_document_client"
5
+
6
+ module RubyLsp
7
+ module Requests
8
+ # ![Hover demo](../../misc/rails_document_link_hover.gif)
9
+ #
10
+ # The [hover request](https://microsoft.github.io/language-server-protocol/specification#textDocument_hover)
11
+ # renders a clickable link to the code's official documentation.
12
+ # It currently only supports Rails' documentation: when hovering over Rails DSLs/constants under certain paths,
13
+ # like `before_save :callback` in `models/post.rb`, it generates a link to `before_save`'s API documentation.
14
+ #
15
+ # # Example
16
+ #
17
+ # ```ruby
18
+ # class Post < ApplicationRecord
19
+ # before_save :do_something # when hovering on before_save, the link will be rendered
20
+ # end
21
+ # ```
22
+ class Hover < BaseRequest
23
+ extend T::Sig
24
+
25
+ sig { params(document: Document, position: Document::PositionShape).void }
26
+ def initialize(document, position)
27
+ super(document)
28
+
29
+ @position = T.let(Document::Scanner.new(document.source).find_position(position), Integer)
30
+ end
31
+
32
+ sig { override.returns(T.nilable(LanguageServer::Protocol::Interface::Hover)) }
33
+ def run
34
+ return unless @document.parsed?
35
+
36
+ target, _ = locate_node_and_parent(
37
+ T.must(@document.tree), [SyntaxTree::Command, SyntaxTree::FCall, SyntaxTree::ConstPathRef], @position
38
+ )
39
+
40
+ case target
41
+ when SyntaxTree::Command
42
+ message = target.message
43
+ generate_rails_document_link_hover(message.value, message)
44
+ when SyntaxTree::FCall
45
+ message = target.value
46
+ generate_rails_document_link_hover(message.value, message)
47
+ when SyntaxTree::ConstPathRef
48
+ constant_name = full_constant_name(target)
49
+ generate_rails_document_link_hover(constant_name, target)
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ sig do
56
+ params(name: String, node: SyntaxTree::Node).returns(T.nilable(LanguageServer::Protocol::Interface::Hover))
57
+ end
58
+ def generate_rails_document_link_hover(name, node)
59
+ urls = Support::RailsDocumentClient.generate_rails_document_urls(name)
60
+
61
+ return if urls.empty?
62
+
63
+ contents = LanguageServer::Protocol::Interface::MarkupContent.new(
64
+ kind: "markdown",
65
+ value: urls.join("\n\n"),
66
+ )
67
+ LanguageServer::Protocol::Interface::Hover.new(
68
+ range: range_from_syntax_tree_node(node),
69
+ contents: contents,
70
+ )
71
+ end
72
+ end
73
+ end
74
+ end
@@ -31,7 +31,7 @@ module RubyLsp
31
31
 
32
32
  sig { override.returns(T.all(T::Array[LanguageServer::Protocol::Interface::InlayHint], Object)) }
33
33
  def run
34
- visit(@document.tree)
34
+ visit(@document.tree) if @document.parsed?
35
35
  @hints
36
36
  end
37
37
 
@@ -70,7 +70,7 @@ module RubyLsp
70
70
 
71
71
  sig { override.returns(T.all(T::Array[Support::SelectionRange], Object)) }
72
72
  def run
73
- visit(@document.tree)
73
+ visit(@document.tree) if @document.parsed?
74
74
  @ranges.reverse!
75
75
  end
76
76
 
@@ -94,6 +94,8 @@ module RubyLsp
94
94
  )
95
95
  end
96
96
  def run
97
+ return @tokens unless @document.parsed?
98
+
97
99
  visit(@tree)
98
100
  return @tokens unless @encoder
99
101
 
@@ -0,0 +1,114 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "net/http"
5
+
6
+ module RubyLsp
7
+ module Requests
8
+ module Support
9
+ class RailsDocumentClient
10
+ RAILS_DOC_HOST = "https://api.rubyonrails.org"
11
+ SUPPORTED_RAILS_DOC_NAMESPACES = T.let(
12
+ Regexp.union(
13
+ /ActionDispatch/, /ActionController/, /AbstractController/, /ActiveRecord/, /ActiveModel/, /ActiveStorage/,
14
+ /ActionText/, /ActiveJob/
15
+ ).freeze,
16
+ Regexp,
17
+ )
18
+
19
+ RAILTIES_VERSION = T.let(
20
+ [*::Gem::Specification.default_stubs, *::Gem::Specification.stubs].find do |s|
21
+ s.name == "railties"
22
+ end&.version&.to_s, T.nilable(String)
23
+ )
24
+
25
+ class << self
26
+ extend T::Sig
27
+ sig do
28
+ params(name: String).returns(T::Array[String])
29
+ end
30
+ def generate_rails_document_urls(name)
31
+ docs = T.must(search_index)[name]
32
+
33
+ return [] unless docs
34
+
35
+ docs.map do |doc|
36
+ owner = doc[:owner]
37
+
38
+ link_name =
39
+ # class/module name
40
+ if owner == name
41
+ name
42
+ else
43
+ "#{owner}##{name}"
44
+ end
45
+
46
+ "[Rails Document: `#{link_name}`](#{doc[:url]})"
47
+ end
48
+ end
49
+
50
+ sig { returns(T.nilable(T::Hash[String, T::Array[T::Hash[Symbol, String]]])) }
51
+ private def search_index
52
+ @rails_documents ||= T.let(
53
+ build_search_index,
54
+ T.nilable(T::Hash[String, T::Array[T::Hash[Symbol, String]]]),
55
+ )
56
+ end
57
+
58
+ sig { returns(T.nilable(T::Hash[String, T::Array[T::Hash[Symbol, String]]])) }
59
+ private def build_search_index
60
+ return unless RAILTIES_VERSION
61
+
62
+ $stderr.puts "Fetching Rails Documents..."
63
+ # If the version's doc is not found, e.g. Rails main, it'll be redirected
64
+ # In this case, we just fetch the latest doc
65
+ response = if Gem::Version.new(RAILTIES_VERSION).prerelease?
66
+ Net::HTTP.get_response(URI("#{RAILS_DOC_HOST}/js/search_index.js"))
67
+ else
68
+ Net::HTTP.get_response(URI("#{RAILS_DOC_HOST}/v#{RAILTIES_VERSION}/js/search_index.js"))
69
+ end
70
+
71
+ if response.code == "200"
72
+ process_search_index(response.body)
73
+ else
74
+ $stderr.puts("Response failed: #{response.inspect}")
75
+ nil
76
+ end
77
+ rescue StandardError => e
78
+ $stderr.puts("Exception occurred when fetching Rails document index: #{e.inspect}")
79
+ end
80
+
81
+ sig { params(js: String).returns(T::Hash[String, T::Array[T::Hash[Symbol, String]]]) }
82
+ private def process_search_index(js)
83
+ raw_data = js.sub("var search_data = ", "")
84
+ info = JSON.parse(raw_data).dig("index", "info")
85
+
86
+ # An entry looks like this:
87
+ #
88
+ # ["belongs_to", # method or module/class
89
+ # "ActiveRecord::Associations::ClassMethods", # method owner
90
+ # "classes/ActiveRecord/Associations/ClassMethods.html#method-i-belongs_to", # path to the document
91
+ # "(name, scope = nil, **options)", # method's parameters
92
+ # "<p>Specifies a one-to-one association with another class..."] # document preview
93
+ #
94
+ info.each_with_object({}) do |(method_or_class, method_owner, doc_path, _, doc_preview), table|
95
+ # If a method doesn't have documentation, there's no need to generate the link to it.
96
+ next if doc_preview.nil? || doc_preview.empty?
97
+
98
+ # If the method or class/module is not from the supported namespace, reject it
99
+ next unless [method_or_class, method_owner].any? do |elem|
100
+ elem.match?(SUPPORTED_RAILS_DOC_NAMESPACES)
101
+ end
102
+
103
+ owner = method_owner.empty? ? method_or_class : method_owner
104
+ table[method_or_class] ||= []
105
+ # It's possible to have multiple modules defining the same method name. For example,
106
+ # both `ActiveRecord::FinderMethods` and `ActiveRecord::Associations::CollectionProxy` defines `#find`
107
+ table[method_or_class] << { owner: owner, url: "#{RAILS_DOC_HOST}/v#{RAILTIES_VERSION}/#{doc_path}" }
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -6,6 +6,7 @@ module RubyLsp
6
6
  #
7
7
  # - {RubyLsp::Requests::DocumentSymbol}
8
8
  # - {RubyLsp::Requests::DocumentLink}
9
+ # - {RubyLsp::Requests::Hover}
9
10
  # - {RubyLsp::Requests::FoldingRanges}
10
11
  # - {RubyLsp::Requests::SelectionRanges}
11
12
  # - {RubyLsp::Requests::SemanticHighlighting}
@@ -20,6 +21,7 @@ module RubyLsp
20
21
  autoload :BaseRequest, "ruby_lsp/requests/base_request"
21
22
  autoload :DocumentSymbol, "ruby_lsp/requests/document_symbol"
22
23
  autoload :DocumentLink, "ruby_lsp/requests/document_link"
24
+ autoload :Hover, "ruby_lsp/requests/hover"
23
25
  autoload :FoldingRanges, "ruby_lsp/requests/folding_ranges"
24
26
  autoload :SelectionRanges, "ruby_lsp/requests/selection_ranges"
25
27
  autoload :SemanticHighlighting, "ruby_lsp/requests/semantic_highlighting"
@@ -23,6 +23,10 @@ module RubyLsp
23
23
  Interface::DocumentLinkOptions.new(resolve_provider: false)
24
24
  end
25
25
 
26
+ hover_provider = if enabled_features.include?("hover")
27
+ Interface::HoverClientCapabilities.new(dynamic_registration: false)
28
+ end
29
+
26
30
  folding_ranges_provider = if enabled_features.include?("foldingRanges")
27
31
  Interface::FoldingRangeClientCapabilities.new(line_folding_only: true)
28
32
  end
@@ -66,6 +70,7 @@ module RubyLsp
66
70
  open_close: true,
67
71
  ),
68
72
  selection_range_provider: enabled_features.include?("selectionRanges"),
73
+ hover_provider: hover_provider,
69
74
  document_symbol_provider: document_symbol_provider,
70
75
  document_link_provider: document_link_provider,
71
76
  folding_range_provider: folding_ranges_provider,
@@ -116,6 +121,13 @@ module RubyLsp
116
121
  end
117
122
  end
118
123
 
124
+ on("textDocument/hover") do |request|
125
+ position = request.dig(:params, :position)
126
+ document = store.get(request.dig(:params, :textDocument, :uri))
127
+
128
+ RubyLsp::Requests::Hover.new(document, position).run
129
+ end
130
+
119
131
  on("textDocument/foldingRange", parallel: true) do |request|
120
132
  store.cache_fetch(request.dig(:params, :textDocument, :uri), :folding_ranges) do |document|
121
133
  Requests::FoldingRanges.new(document).run
@@ -175,9 +187,7 @@ module RubyLsp
175
187
  on("textDocument/documentHighlight", parallel: true) do |request|
176
188
  document = store.get(request.dig(:params, :textDocument, :uri))
177
189
 
178
- if document.parsed?
179
- Requests::DocumentHighlight.new(document, request.dig(:params, :position)).run
180
- end
190
+ Requests::DocumentHighlight.new(document, request.dig(:params, :position)).run
181
191
  end
182
192
 
183
193
  on("textDocument/codeAction", parallel: true) do |request|
@@ -197,9 +207,7 @@ module RubyLsp
197
207
  start_line = range.dig(:start, :line)
198
208
  end_line = range.dig(:end, :line)
199
209
 
200
- if document.parsed?
201
- Requests::InlayHints.new(document, start_line..end_line).run
202
- end
210
+ Requests::InlayHints.new(document, start_line..end_line).run
203
211
  end
204
212
 
205
213
  on("$/cancelRequest") do |request|
@@ -55,13 +55,10 @@ module RubyLsp
55
55
  uri: String,
56
56
  request_name: Symbol,
57
57
  block: T.proc.params(document: Document).returns(T.type_parameter(:T)),
58
- ).returns(T.nilable(T.type_parameter(:T)))
58
+ ).returns(T.type_parameter(:T))
59
59
  end
60
60
  def cache_fetch(uri, request_name, &block)
61
- document = get(uri)
62
- return unless document.parsed?
63
-
64
- document.cache_fetch(request_name, &block)
61
+ get(uri).cache_fetch(request_name, &block)
65
62
  end
66
63
  end
67
64
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-lsp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-10-03 00:00:00.000000000 Z
11
+ date: 2022-10-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: language_server-protocol
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '3.4'
47
+ version: 3.6.3
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: '3.4'
54
+ version: 3.6.3
55
55
  description: An opinionated language server for Ruby
56
56
  email:
57
57
  - ruby@shopify.com
@@ -78,11 +78,13 @@ files:
78
78
  - lib/ruby_lsp/requests/document_symbol.rb
79
79
  - lib/ruby_lsp/requests/folding_ranges.rb
80
80
  - lib/ruby_lsp/requests/formatting.rb
81
+ - lib/ruby_lsp/requests/hover.rb
81
82
  - lib/ruby_lsp/requests/inlay_hints.rb
82
83
  - lib/ruby_lsp/requests/on_type_formatting.rb
83
84
  - lib/ruby_lsp/requests/selection_ranges.rb
84
85
  - lib/ruby_lsp/requests/semantic_highlighting.rb
85
86
  - lib/ruby_lsp/requests/support/highlight_target.rb
87
+ - lib/ruby_lsp/requests/support/rails_document_client.rb
86
88
  - lib/ruby_lsp/requests/support/rubocop_diagnostic.rb
87
89
  - lib/ruby_lsp/requests/support/rubocop_diagnostics_runner.rb
88
90
  - lib/ruby_lsp/requests/support/rubocop_formatting_runner.rb