ruby-lsp 0.4.4 → 0.5.0

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