ruby-lsp 0.3.3 → 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
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