ruby-lsp 0.4.4 → 0.5.0

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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -86
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp +10 -1
  5. data/lib/ruby_lsp/check_docs.rb +112 -0
  6. data/lib/ruby_lsp/document.rb +13 -2
  7. data/lib/ruby_lsp/event_emitter.rb +86 -18
  8. data/lib/ruby_lsp/executor.rb +146 -45
  9. data/lib/ruby_lsp/extension.rb +104 -0
  10. data/lib/ruby_lsp/internal.rb +2 -0
  11. data/lib/ruby_lsp/listener.rb +14 -11
  12. data/lib/ruby_lsp/requests/base_request.rb +0 -5
  13. data/lib/ruby_lsp/requests/code_action_resolve.rb +1 -1
  14. data/lib/ruby_lsp/requests/code_actions.rb +1 -1
  15. data/lib/ruby_lsp/requests/code_lens.rb +82 -66
  16. data/lib/ruby_lsp/requests/diagnostics.rb +2 -2
  17. data/lib/ruby_lsp/requests/document_highlight.rb +1 -1
  18. data/lib/ruby_lsp/requests/document_link.rb +17 -15
  19. data/lib/ruby_lsp/requests/document_symbol.rb +51 -31
  20. data/lib/ruby_lsp/requests/folding_ranges.rb +1 -1
  21. data/lib/ruby_lsp/requests/formatting.rb +10 -11
  22. data/lib/ruby_lsp/requests/hover.rb +34 -19
  23. data/lib/ruby_lsp/requests/inlay_hints.rb +1 -1
  24. data/lib/ruby_lsp/requests/on_type_formatting.rb +5 -1
  25. data/lib/ruby_lsp/requests/path_completion.rb +21 -57
  26. data/lib/ruby_lsp/requests/selection_ranges.rb +1 -1
  27. data/lib/ruby_lsp/requests/semantic_highlighting.rb +1 -1
  28. data/lib/ruby_lsp/requests/support/common.rb +36 -0
  29. data/lib/ruby_lsp/requests/support/rubocop_diagnostics_runner.rb +0 -1
  30. data/lib/ruby_lsp/requests/support/rubocop_formatting_runner.rb +0 -1
  31. data/lib/ruby_lsp/requests/support/syntax_tree_formatting_runner.rb +5 -2
  32. data/lib/ruby_lsp/requests.rb +15 -15
  33. data/lib/ruby_lsp/server.rb +44 -11
  34. data/lib/ruby_lsp/store.rb +1 -1
  35. data/lib/ruby_lsp/utils.rb +9 -8
  36. metadata +5 -3
@@ -3,7 +3,7 @@
3
3
 
4
4
  module RubyLsp
5
5
  module Requests
6
- # ![Path completion demo](../../misc/path_completion.gif)
6
+ # ![Path completion demo](../../path_completion.gif)
7
7
  #
8
8
  # The [completion](https://microsoft.github.io/language-server-protocol/specification#textDocument_completion)
9
9
  # request looks up Ruby files in the $LOAD_PATH to suggest path completion inside `require` statements.
@@ -13,29 +13,28 @@ module RubyLsp
13
13
  # ```ruby
14
14
  # require "ruby_lsp/requests" # --> completion: suggests `base_request`, `code_actions`, ...
15
15
  # ```
16
- class PathCompletion < BaseRequest
16
+ class PathCompletion < Listener
17
17
  extend T::Sig
18
+ extend T::Generic
18
19
 
19
- sig { params(document: Document, position: Document::PositionShape).void }
20
- def initialize(document, position)
21
- super(document)
20
+ ResponseType = type_member { { fixed: T::Array[Interface::CompletionItem] } }
22
21
 
23
- @tree = T.let(Support::PrefixTree.new(collect_load_path_files), Support::PrefixTree)
24
- @position = position
25
- end
22
+ sig { override.returns(ResponseType) }
23
+ attr_reader :response
26
24
 
27
- sig { override.returns(T.all(T::Array[Interface::CompletionItem], Object)) }
28
- def run
29
- # We can't verify if we're inside a require when there are syntax errors
30
- return [] if @document.syntax_error?
25
+ sig { params(emitter: EventEmitter, message_queue: Thread::Queue).void }
26
+ def initialize(emitter, message_queue)
27
+ super
28
+ @response = T.let([], ResponseType)
29
+ @tree = T.let(Support::PrefixTree.new(collect_load_path_files), Support::PrefixTree)
31
30
 
32
- target = T.let(find, T.nilable(SyntaxTree::TStringContent))
33
- # no target means the we are not inside a `require` call
34
- return [] unless target
31
+ emitter.register(self, :on_tstring_content)
32
+ end
35
33
 
36
- text = target.value
37
- @tree.search(text).sort.map! do |path|
38
- build_completion(path, path.delete_prefix(text))
34
+ sig { params(node: SyntaxTree::TStringContent).void }
35
+ def on_tstring_content(node)
36
+ @tree.search(node.value).sort.each do |path|
37
+ @response << build_completion(path, node)
39
38
  end
40
39
  end
41
40
 
@@ -50,48 +49,13 @@ module RubyLsp
50
49
  end
51
50
  end
52
51
 
53
- sig { returns(T.nilable(SyntaxTree::TStringContent)) }
54
- def find
55
- char_position = @document.create_scanner.find_char_position(@position)
56
- matched, parent = @document.locate(
57
- T.must(@document.tree),
58
- char_position,
59
- node_types: [SyntaxTree::Command, SyntaxTree::CommandCall, SyntaxTree::CallNode],
60
- )
61
-
62
- return unless matched && parent
63
-
64
- case matched
65
- when SyntaxTree::Command, SyntaxTree::CallNode, SyntaxTree::CommandCall
66
- message = matched.message
67
- return if message.is_a?(Symbol)
68
- return unless message.value == "require"
69
-
70
- args = matched.arguments
71
- args = args.arguments if args.is_a?(SyntaxTree::ArgParen)
72
- return if args.nil? || args.is_a?(SyntaxTree::ArgsForward)
73
-
74
- argument = args.parts.first
75
- return unless argument.is_a?(SyntaxTree::StringLiteral)
76
-
77
- path_node = argument.parts.first
78
- return unless path_node.is_a?(SyntaxTree::TStringContent)
79
- return unless (path_node.location.start_char..path_node.location.end_char).cover?(char_position)
80
-
81
- path_node
82
- end
83
- end
84
-
85
- sig { params(label: String, insert_text: String).returns(Interface::CompletionItem) }
86
- def build_completion(label, insert_text)
52
+ sig { params(label: String, node: SyntaxTree::TStringContent).returns(Interface::CompletionItem) }
53
+ def build_completion(label, node)
87
54
  Interface::CompletionItem.new(
88
55
  label: label,
89
56
  text_edit: Interface::TextEdit.new(
90
- range: Interface::Range.new(
91
- start: @position,
92
- end: @position,
93
- ),
94
- new_text: insert_text,
57
+ range: range_from_syntax_tree_node(node),
58
+ new_text: label,
95
59
  ),
96
60
  kind: Constant::CompletionItemKind::REFERENCE,
97
61
  )
@@ -3,7 +3,7 @@
3
3
 
4
4
  module RubyLsp
5
5
  module Requests
6
- # ![Selection ranges demo](../../misc/selection_ranges.gif)
6
+ # ![Selection ranges demo](../../selection_ranges.gif)
7
7
  #
8
8
  # The [selection ranges](https://microsoft.github.io/language-server-protocol/specification#textDocument_selectionRange)
9
9
  # request informs the editor of ranges that the user may want to select based on the location(s)
@@ -3,7 +3,7 @@
3
3
 
4
4
  module RubyLsp
5
5
  module Requests
6
- # ![Semantic highlighting demo](../../misc/semantic_highlighting.gif)
6
+ # ![Semantic highlighting demo](../../semantic_highlighting.gif)
7
7
  #
8
8
  # The [semantic
9
9
  # highlighting](https://microsoft.github.io/language-server-protocol/specification#textDocument_semanticTokens)
@@ -49,6 +49,42 @@ module RubyLsp
49
49
  loc = node.location
50
50
  range.cover?(loc.start_line - 1) && range.cover?(loc.end_line - 1)
51
51
  end
52
+
53
+ sig do
54
+ params(
55
+ node: SyntaxTree::Node,
56
+ title: String,
57
+ command_name: String,
58
+ path: String,
59
+ name: String,
60
+ test_command: String,
61
+ type: String,
62
+ ).returns(Interface::CodeLens)
63
+ end
64
+ def create_code_lens(node, title:, command_name:, path:, name:, test_command:, type:)
65
+ range = range_from_syntax_tree_node(node)
66
+ arguments = [
67
+ path,
68
+ name,
69
+ test_command,
70
+ {
71
+ start_line: node.location.start_line - 1,
72
+ start_column: node.location.start_column,
73
+ end_line: node.location.end_line - 1,
74
+ end_column: node.location.end_column,
75
+ },
76
+ ]
77
+
78
+ Interface::CodeLens.new(
79
+ range: range,
80
+ command: Interface::Command.new(
81
+ title: title,
82
+ command: command_name,
83
+ arguments: arguments,
84
+ ),
85
+ data: { type: type },
86
+ )
87
+ end
52
88
  end
53
89
  end
54
90
  end
@@ -1,7 +1,6 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "ruby_lsp/requests/support/rubocop_runner"
5
4
  return unless defined?(RubyLsp::Requests::Support::RuboCopRunner)
6
5
 
7
6
  require "cgi"
@@ -1,7 +1,6 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "ruby_lsp/requests/support/rubocop_runner"
5
4
  return unless defined?(RubyLsp::Requests::Support::RuboCopRunner)
6
5
 
7
6
  require "cgi"
@@ -25,8 +25,11 @@ module RubyLsp
25
25
  )
26
26
  end
27
27
 
28
- sig { params(_uri: String, document: Document).returns(T.nilable(String)) }
29
- def run(_uri, document)
28
+ sig { params(uri: String, document: Document).returns(T.nilable(String)) }
29
+ def run(uri, document)
30
+ relative_path = Pathname.new(URI(uri).path).relative_path_from(T.must(WORKSPACE_URI.path))
31
+ return if @options.ignore_files.any? { |pattern| File.fnmatch(pattern, relative_path) }
32
+
30
33
  SyntaxTree.format(
31
34
  document.source,
32
35
  @options.print_width,
@@ -4,21 +4,21 @@
4
4
  module RubyLsp
5
5
  # Supported features
6
6
  #
7
- # - {RubyLsp::Requests::DocumentSymbol}
8
- # - {RubyLsp::Requests::DocumentLink}
9
- # - {RubyLsp::Requests::Hover}
10
- # - {RubyLsp::Requests::FoldingRanges}
11
- # - {RubyLsp::Requests::SelectionRanges}
12
- # - {RubyLsp::Requests::SemanticHighlighting}
13
- # - {RubyLsp::Requests::Formatting}
14
- # - {RubyLsp::Requests::OnTypeFormatting}
15
- # - {RubyLsp::Requests::Diagnostics}
16
- # - {RubyLsp::Requests::CodeActions}
17
- # - {RubyLsp::Requests::CodeActionResolve}
18
- # - {RubyLsp::Requests::DocumentHighlight}
19
- # - {RubyLsp::Requests::InlayHints}
20
- # - {RubyLsp::Requests::PathCompletion}
21
- # - {RubyLsp::Requests::CodeLens}
7
+ # - [DocumentSymbol](rdoc-ref:RubyLsp::Requests::DocumentSymbol)
8
+ # - [DocumentLink](rdoc-ref:RubyLsp::Requests::DocumentLink)
9
+ # - [Hover](rdoc-ref:RubyLsp::Requests::Hover)
10
+ # - [FoldingRange](rdoc-ref:RubyLsp::Requests::FoldingRanges)
11
+ # - [SelectionRange](rdoc-ref:RubyLsp::Requests::SelectionRanges)
12
+ # - [SemanticHighlighting](rdoc-ref:RubyLsp::Requests::SemanticHighlighting)
13
+ # - [Formatting](rdoc-ref:RubyLsp::Requests::Formatting)
14
+ # - [OnTypeFormatting](rdoc-ref:RubyLsp::Requests::OnTypeFormatting)
15
+ # - [Diagnostic](rdoc-ref:RubyLsp::Requests::Diagnostics)
16
+ # - [CodeAction](rdoc-ref:RubyLsp::Requests::CodeActions)
17
+ # - [CodeActionResolve](rdoc-ref:RubyLsp::Requests::CodeActionResolve)
18
+ # - [DocumentHighlight](rdoc-ref:RubyLsp::Requests::DocumentHighlight)
19
+ # - [InlayHint](rdoc-ref:RubyLsp::Requests::InlayHints)
20
+ # - [PathCompletion](rdoc-ref:RubyLsp::Requests::PathCompletion)
21
+ # - [CodeLens](rdoc-ref:RubyLsp::Requests::CodeLens)
22
22
 
23
23
  module Requests
24
24
  autoload :BaseRequest, "ruby_lsp/requests/base_request"
@@ -24,6 +24,32 @@ module RubyLsp
24
24
  @mutex = T.let(Mutex.new, Mutex)
25
25
  @worker = T.let(new_worker, Thread)
26
26
 
27
+ # The messages queue includes requests and notifications to be sent to the client
28
+ @message_queue = T.let(Thread::Queue.new, Thread::Queue)
29
+
30
+ # Create a thread to watch the messages queue and send them to the client
31
+ @message_dispatcher = T.let(
32
+ Thread.new do
33
+ current_request_id = 1
34
+
35
+ loop do
36
+ message = @message_queue.pop
37
+ break if message.nil?
38
+
39
+ @mutex.synchronize do
40
+ case message
41
+ when Notification
42
+ @writer.write(method: message.message, params: message.params)
43
+ when Request
44
+ @writer.write(id: current_request_id, method: message.message, params: message.params)
45
+ current_request_id += 1
46
+ end
47
+ end
48
+ end
49
+ end,
50
+ Thread,
51
+ )
52
+
27
53
  Thread.main.priority = 1
28
54
  end
29
55
 
@@ -35,9 +61,8 @@ module RubyLsp
35
61
  # fall under the else branch which just pushes requests to the queue
36
62
  @reader.read do |request|
37
63
  case request[:method]
38
- when "initialize", "initialized", "textDocument/didOpen", "textDocument/didClose", "textDocument/didChange",
39
- "textDocument/formatting", "textDocument/onTypeFormatting", "codeAction/resolve"
40
- result = Executor.new(@store).execute(request)
64
+ when "initialize", "initialized", "textDocument/didOpen", "textDocument/didClose", "textDocument/didChange"
65
+ result = Executor.new(@store, @message_queue).execute(request)
41
66
  finalize_request(result, request)
42
67
  when "$/cancelRequest"
43
68
  # Cancel the job if it's still in the queue
@@ -45,6 +70,7 @@ module RubyLsp
45
70
  when "shutdown"
46
71
  warn("Shutting down Ruby LSP...")
47
72
 
73
+ @message_queue.close
48
74
  # Close the queue so that we can no longer receive items
49
75
  @job_queue.close
50
76
  # Clear any remaining jobs so that the thread can terminate
@@ -52,9 +78,10 @@ module RubyLsp
52
78
  @jobs.clear
53
79
  # Wait until the thread is finished
54
80
  @worker.join
81
+ @message_dispatcher.join
55
82
  @store.clear
56
83
 
57
- finalize_request(Result.new(response: nil, notifications: []), request)
84
+ finalize_request(Result.new(response: nil), request)
58
85
  when "exit"
59
86
  # We return zero if shutdown has already been received or one otherwise as per the recommendation in the spec
60
87
  # https://microsoft.github.io/language-server-protocol/specification/#exit
@@ -65,8 +92,17 @@ module RubyLsp
65
92
  # Default case: push the request to the queue to be executed by the worker
66
93
  job = Job.new(request: request, cancelled: false)
67
94
 
68
- # Remember a handle to the job, so that we can cancel it
69
- @mutex.synchronize { @jobs[request[:id]] = job }
95
+ @mutex.synchronize do
96
+ # Remember a handle to the job, so that we can cancel it
97
+ @jobs[request[:id]] = job
98
+
99
+ # We must parse the document under a mutex lock or else we might switch threads and accept text edits in the
100
+ # source. Altering the source reference during parsing will put the parser in an invalid internal state,
101
+ # since it started parsing with one source but then it changed in the middle
102
+ uri = request.dig(:params, :textDocument, :uri)
103
+ @store.get(uri).parse if uri
104
+ end
105
+
70
106
  @job_queue << job
71
107
  end
72
108
  end
@@ -88,9 +124,9 @@ module RubyLsp
88
124
 
89
125
  result = if job.cancelled
90
126
  # We need to return nil to the client even if the request was cancelled
91
- Result.new(response: nil, notifications: [])
127
+ Result.new(response: nil)
92
128
  else
93
- Executor.new(@store).execute(request)
129
+ Executor.new(@store, @message_queue).execute(request)
94
130
  end
95
131
 
96
132
  finalize_request(result, request)
@@ -105,9 +141,6 @@ module RubyLsp
105
141
  error = result.error
106
142
  response = result.response
107
143
 
108
- # If the response include any notifications, go through them and publish each one
109
- result.notifications.each { |n| @writer.write(method: n.message, params: n.params) }
110
-
111
144
  if error
112
145
  @writer.write(
113
146
  id: request[:id],
@@ -61,7 +61,7 @@ module RubyLsp
61
61
  type_parameters(:T)
62
62
  .params(
63
63
  uri: String,
64
- request_name: Symbol,
64
+ request_name: String,
65
65
  block: T.proc.params(document: Document).returns(T.type_parameter(:T)),
66
66
  ).returns(T.type_parameter(:T))
67
67
  end
@@ -6,11 +6,14 @@ module RubyLsp
6
6
  VOID = T.let(Object.new.freeze, Object)
7
7
 
8
8
  # This freeze is not redundant since the interpolated string is mutable
9
- WORKSPACE_URI = T.let("file://#{Dir.pwd}".freeze, String) # rubocop:disable Style/RedundantFreeze
9
+ WORKSPACE_URI = T.let(URI("file://#{Dir.pwd}".freeze), URI::Generic) # rubocop:disable Style/RedundantFreeze
10
10
 
11
11
  # A notification to be sent to the client
12
- class Notification
12
+ class Message
13
13
  extend T::Sig
14
+ extend T::Helpers
15
+
16
+ abstract!
14
17
 
15
18
  sig { returns(String) }
16
19
  attr_reader :message
@@ -25,6 +28,9 @@ module RubyLsp
25
28
  end
26
29
  end
27
30
 
31
+ class Notification < Message; end
32
+ class Request < Message; end
33
+
28
34
  # The final result of running a request before its IO is finalized
29
35
  class Result
30
36
  extend T::Sig
@@ -32,9 +38,6 @@ module RubyLsp
32
38
  sig { returns(T.untyped) }
33
39
  attr_reader :response
34
40
 
35
- sig { returns(T::Array[Notification]) }
36
- attr_reader :notifications
37
-
38
41
  sig { returns(T.nilable(Exception)) }
39
42
  attr_reader :error
40
43
 
@@ -44,14 +47,12 @@ module RubyLsp
44
47
  sig do
45
48
  params(
46
49
  response: T.untyped,
47
- notifications: T::Array[Notification],
48
50
  error: T.nilable(Exception),
49
51
  request_time: T.nilable(Float),
50
52
  ).void
51
53
  end
52
- def initialize(response:, notifications:, error: nil, request_time: nil)
54
+ def initialize(response:, error: nil, request_time: nil)
53
55
  @response = response
54
- @notifications = notifications
55
56
  @error = error
56
57
  @request_time = request_time
57
58
  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.4.4
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-04-11 00:00:00.000000000 Z
11
+ date: 2023-05-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: language_server-protocol
@@ -72,9 +72,11 @@ files:
72
72
  - exe/ruby-lsp
73
73
  - lib/rubocop/cop/ruby_lsp/use_language_server_aliases.rb
74
74
  - lib/ruby-lsp.rb
75
+ - lib/ruby_lsp/check_docs.rb
75
76
  - lib/ruby_lsp/document.rb
76
77
  - lib/ruby_lsp/event_emitter.rb
77
78
  - lib/ruby_lsp/executor.rb
79
+ - lib/ruby_lsp/extension.rb
78
80
  - lib/ruby_lsp/internal.rb
79
81
  - lib/ruby_lsp/listener.rb
80
82
  - lib/ruby_lsp/requests.rb
@@ -131,7 +133,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
131
133
  - !ruby/object:Gem::Version
132
134
  version: '0'
133
135
  requirements: []
134
- rubygems_version: 3.4.10
136
+ rubygems_version: 3.4.12
135
137
  signing_key:
136
138
  specification_version: 4
137
139
  summary: An opinionated language server for Ruby