ruby-lsp 0.4.5 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -86
  3. data/VERSION +1 -1
  4. data/lib/ruby_lsp/check_docs.rb +112 -0
  5. data/lib/ruby_lsp/document.rb +13 -2
  6. data/lib/ruby_lsp/event_emitter.rb +84 -18
  7. data/lib/ruby_lsp/executor.rb +126 -48
  8. data/lib/ruby_lsp/internal.rb +1 -0
  9. data/lib/ruby_lsp/listener.rb +6 -13
  10. data/lib/ruby_lsp/requests/base_request.rb +0 -5
  11. data/lib/ruby_lsp/requests/code_action_resolve.rb +1 -1
  12. data/lib/ruby_lsp/requests/code_actions.rb +1 -1
  13. data/lib/ruby_lsp/requests/code_lens.rb +82 -66
  14. data/lib/ruby_lsp/requests/diagnostics.rb +2 -2
  15. data/lib/ruby_lsp/requests/document_highlight.rb +1 -1
  16. data/lib/ruby_lsp/requests/document_link.rb +17 -15
  17. data/lib/ruby_lsp/requests/document_symbol.rb +51 -31
  18. data/lib/ruby_lsp/requests/folding_ranges.rb +1 -1
  19. data/lib/ruby_lsp/requests/formatting.rb +10 -11
  20. data/lib/ruby_lsp/requests/hover.rb +20 -20
  21. data/lib/ruby_lsp/requests/inlay_hints.rb +1 -1
  22. data/lib/ruby_lsp/requests/on_type_formatting.rb +1 -1
  23. data/lib/ruby_lsp/requests/path_completion.rb +21 -57
  24. data/lib/ruby_lsp/requests/selection_ranges.rb +1 -1
  25. data/lib/ruby_lsp/requests/semantic_highlighting.rb +1 -1
  26. data/lib/ruby_lsp/requests/support/common.rb +36 -0
  27. data/lib/ruby_lsp/requests/support/rubocop_diagnostics_runner.rb +0 -1
  28. data/lib/ruby_lsp/requests/support/rubocop_formatting_runner.rb +0 -1
  29. data/lib/ruby_lsp/requests/support/syntax_tree_formatting_runner.rb +5 -2
  30. data/lib/ruby_lsp/requests.rb +15 -15
  31. data/lib/ruby_lsp/server.rb +44 -20
  32. data/lib/ruby_lsp/store.rb +1 -1
  33. data/lib/ruby_lsp/utils.rb +2 -7
  34. metadata +3 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 437c2048862bed450fd70057489636117c99943dd7bb68df35c4e161a29f8385
4
- data.tar.gz: 30d26e8455f9086bc934ffe23b5e37816f18f5904e3422495c316a980a860cfc
3
+ metadata.gz: f8f2f2aa1083dcc700e709ebd0bc3d3aed8f08d80943595ba3c9df332d806709
4
+ data.tar.gz: a065d2f6be6539c81ad117bfba7c7b4f90ead0bbce37dd936733218e3135bc67
5
5
  SHA512:
6
- metadata.gz: 366dd4b756ded58762b598545e398059fe0eae7578b13c92cc0a90dcc9aba2d58106dbf2dc0e9867a9db7de5007e6114ebbbf7cf191bcdebb843b077de945ba9
7
- data.tar.gz: 1f798c1462825cccafd48bff23baecb8ba8ef36f9b5733502d79c5d8e6e52550c7ab722a99678eec04ce2a8b42c4d84aab912acb2100ff7d26e422b02f2c185b
6
+ metadata.gz: de010f852e08fd72ac60a12cbb9a8666a92945c49b8a14214f0e347c9a6fbacb65770322627db22a30b573ee61f34ec18be56d625768a6c954cf447dbda63368
7
+ data.tar.gz: 3f47e62896899091c4a9b166937720cc535f46004827a098bfb7eaf9c31fb39bb35261077b91a01a41baa18a50482d8ffe1b8ebddabaed1ea15bc0d023e76dae
data/README.md CHANGED
@@ -2,7 +2,6 @@
2
2
  [![Ruby LSP extension](https://img.shields.io/badge/VS%20Code-Ruby%20LSP-success?logo=visual-studio-code)](https://marketplace.visualstudio.com/items?itemName=Shopify.ruby-lsp)
3
3
  [![Ruby DX Slack](https://img.shields.io/badge/Slack-Ruby%20DX-success?logo=slack)](https://join.slack.com/t/ruby-dx/shared_invite/zt-1s6f4y15t-v9jedZ9YUPQLM91TEJ4Gew)
4
4
 
5
-
6
5
  # Ruby LSP
7
6
 
8
7
  The Ruby LSP is an implementation of the [language server protocol](https://microsoft.github.io/language-server-protocol/)
@@ -21,8 +20,7 @@ get the extra features in the editor. Do not install this gem manually.
21
20
 
22
21
  ### With other editors
23
22
 
24
- See [editors](https://github.com/Shopify/ruby-lsp/blob/main/EDITORS.md) for community instructions on setting up the
25
- Ruby LSP.
23
+ See [editors](EDITORS.md) for community instructions on setting up the Ruby LSP.
26
24
 
27
25
  The gem can be installed by doing
28
26
  ```shell
@@ -51,92 +49,13 @@ See the [documentation](https://shopify.github.io/ruby-lsp) for more in-depth de
51
49
  Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/ruby-lsp.
52
50
  This project is intended to be a safe, welcoming space for collaboration, and contributors
53
51
  are expected to adhere to the
54
- [Contributor Covenant](https://github.com/Shopify/ruby-lsp/blob/main/CODE_OF_CONDUCT.md)
52
+ [Contributor Covenant](CODE_OF_CONDUCT.md)
55
53
  code of conduct.
56
54
 
57
- ### Running the test suite
58
-
59
- Run the test suite with `bin/test`.
60
-
61
- For more visibility into which tests are running, use the `SpecReporter`:
62
-
63
- `SPEC_REPORTER=1 bin/test`
64
-
65
- By default the tests run with warnings disabled to reduce noise. To enable warnings, pass `VERBOSE=1`.
66
- Warnings are always shown when running in CI.
67
-
68
- ### Expectation testing
69
-
70
- To simplify the way we run tests over different pieces of Ruby code, we use a custom expectations test framework against a set of Ruby fixtures.
71
-
72
- We define expectations as `.exp` files, of which there are two variants:
73
- * `.exp.rb`, to indicate the resulting code after an operation.
74
- * `.exp.json`, consisting of a `result`, and an optional set of input `params`.
75
-
76
- To add a new fixture to the expectations test suite:
77
-
78
- 1. Add a new fixture `my_fixture.rb` file under `test/fixtures`
79
- 2. (optional) Add new expectations under `test/expectations/$HANDLER` for the request handlers you're concerned by
80
- 3. Profit by running `bin/test test/requests/$HANDLER_expectations_test my_fixture`
81
- * Handlers for which you added expectations will be checked with `assert_expectations`
82
- * Handlers without expectations will be ran against your new test to check that nothing breaks
83
-
84
- To add a new expectations test runner for a new request handler:
85
-
86
- 1. Add a new file under `test/requests/$HANDLER_expectations_test.rb` that subclasses `ExpectationsTestRunner` and calls `expectations_tests $HANDLER, "$EXPECTATIONS_DIR"` where: `$HANDLER` is the fully qualified name or your handler class and `$EXPECTATIONS_DIR` is the directory name where you want to store the expectation files.
87
-
88
- ```rb
89
- # frozen_string_literal: true
90
-
91
- require "test_helper"
92
- require "expectations/expectations_test_runner"
93
-
94
- class $HANDLERExpectationsTest < ExpectationsTestRunner
95
- expectations_tests RubyLsp::Requests::$HANDLER, "$EXPECTATIONS_DIR"
96
- end
97
- ```
98
-
99
- 2. (optional) Override the `run_expectations` and `assert_expectations` methods if needed. See the different request handler expectations runners under `test/requests/*_expectations_test.rb` for examples.
100
-
101
- 4. (optional) Add new fixtures for your handler under `test/fixtures`
102
-
103
- 5. (optional) Add new expectations under `test/expectations/$HANDLER`
104
- * No need to write the expectations by hand, just run the test with an empty expectation file and copy from the output.
105
-
106
- 7. Profit by running, `bin/test test/expectations_test $HANDLER`
107
- * Tests with expectations will be checked with `assert_expectations`
108
- * Tests without expectations will be ran against your new $HANDLER to check that nothing breaks
109
-
110
- ### Debugging
111
-
112
- ### Debugging Tests
113
-
114
- 1. Open the test file.
115
- 2. Set a breakpoint(s) on lines by clicking next to their numbers.
116
- 3. Open VS Code's `Run and Debug` panel.
117
- 4. At the top of the panel, select `Minitset - current file` and click the green triangle (or press F5).
118
-
119
- ### Debugging Running Ruby LSP Process
120
-
121
- 1. Open the `vscode-ruby-lsp` project in VS Code.
122
- 2. [`vscode-ruby-lsp`] Open VS Code's `Run and Debug` panel.
123
- 3. [`vscode-ruby-lsp`] Select `Run Extension` and click the green triangle (or press F5).
124
- 4. [`vscode-ruby-lsp`] Now VS Code will:
125
- - Open another workspace as the `Extension Development Host`.
126
- - Run `vscode-ruby-lsp` extension in debug mode, which will start a new `ruby-lsp` process with the `--debug` flag. Note that debugging is not available on Windows.
127
- 5. Open `ruby-lsp` in VS Code.
128
- 6. [`ruby-lsp`] Run `bin/rdbg -A` to connect to the running `ruby-lsp` process.
129
- 7. [`ruby-lsp`] Use commands like `b <file>:<line>` or `b Class#method` to set breakpoints and type `c` to continue the process.
130
- 8. In your `Extension Development Host` project (e.g. [`Tapioca`](https://github.com/Shopify/tapioca)), trigger the request that will hit the breakpoint.
131
-
132
- ### Spell Checking
133
-
134
- VS Code users will be prompted to enable the [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) extension.
135
- By default this will be enabled for all workspaces, but you can choose to selectively enable or disable it per workspace.
136
-
137
- If you introduce a word which the spell checker does not recognize, you can add it to the `cspell.json` configuration alongside your PR.
55
+ If you wish to contribute, see [CONTRIBUTING](CONTRIBUTING.md) for development instructions and check out our pinned
56
+ [roadmap issue](https://github.com/Shopify/ruby-lsp/issues) for a list of tasks to get started.
138
57
 
139
58
  ## License
140
59
 
141
60
  The gem is available as open source under the terms of the
142
- [MIT License](https://github.com/Shopify/ruby-lsp/blob/main/LICENSE.txt).
61
+ [MIT License](LICENSE.txt).
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.5
1
+ 0.5.0
@@ -0,0 +1,112 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "ruby_lsp/internal"
5
+ require "objspace"
6
+
7
+ module RubyLsp
8
+ # This rake task checks that all requests or extensions are fully documented. Add the rake task to your Rakefile and
9
+ # specify the absolute path for all files that must be required in order to discover all listeners
10
+ #
11
+ # # Rakefile
12
+ # request_files = FileList.new("#{__dir__}/lib/ruby_lsp/requests/*.rb") do |fl|
13
+ # fl.exclude(/base_request\.rb/)
14
+ # end
15
+ # RubyLsp::CheckDocs.new(request_files)
16
+ # # Run with bundle exec rake ruby_lsp:check_docs
17
+ class CheckDocs < Rake::TaskLib
18
+ extend T::Sig
19
+
20
+ sig { params(require_files: Rake::FileList).void }
21
+ def initialize(require_files)
22
+ super()
23
+
24
+ @name = T.let("ruby_lsp:check_docs", String)
25
+ @file_list = require_files
26
+ define_task
27
+ end
28
+
29
+ private
30
+
31
+ sig { void }
32
+ def define_task
33
+ desc("Checks if all Ruby LSP listeners are documented")
34
+ task(@name) { run_task }
35
+ end
36
+
37
+ sig { void }
38
+ def run_task
39
+ # Require all files configured to make sure all listeners are loaded
40
+ @file_list.each { |f| require(f.delete_suffix(".rb")) }
41
+
42
+ # Find all classes that inherit from BaseRequest or Listener, which are the ones we want to make sure are
43
+ # documented
44
+ features = ObjectSpace.each_object(Class).filter_map do |k|
45
+ klass = T.cast(k, Class)
46
+ klass if klass < RubyLsp::Requests::BaseRequest || klass < RubyLsp::Listener
47
+ end
48
+
49
+ missing_docs = T.let(Hash.new { |h, k| h[k] = [] }, T::Hash[String, T::Array[String]])
50
+
51
+ features.each do |klass|
52
+ class_name = T.must(klass.name)
53
+ file_path, line_number = Module.const_source_location(class_name)
54
+ next unless file_path && line_number
55
+
56
+ # Adjust the line number to start searching right above the class definition
57
+ line_number -= 2
58
+
59
+ lines = File.readlines(file_path)
60
+ docs = []
61
+
62
+ # Extract the documentation on top of the listener constant
63
+ while (line = lines[line_number]&.strip) && line.start_with?("#")
64
+ docs.unshift(line)
65
+ line_number -= 1
66
+ end
67
+
68
+ documentation = docs.join("\n")
69
+
70
+ if docs.empty?
71
+ T.must(missing_docs[class_name]) << "No documentation found"
72
+ elsif !%r{\(https://microsoft.github.io/language-server-protocol/specification#.*\)}.match?(documentation)
73
+ T.must(missing_docs[class_name]) << <<~DOCS
74
+ Missing specification link. Requests and extensions should include a link to the LSP specification for the
75
+ related feature. For example:
76
+
77
+ [Inlay hint](https://microsoft.github.io/language-server-protocol/specification#textDocument_inlayHint)
78
+ DOCS
79
+ elsif !documentation.include?("# Example")
80
+ T.must(missing_docs[class_name]) << <<~DOCS
81
+ Missing example. Requests and extensions should include a code example that explains what the feature does.
82
+
83
+ # # Example
84
+ # ```ruby
85
+ # class Foo # <- information is shown here
86
+ # end
87
+ # ```
88
+ DOCS
89
+ elsif !/\[.* demo\]\(.*\.gif\)/.match?(documentation)
90
+ T.must(missing_docs[class_name]) << <<~DOCS
91
+ Missing demonstration GIF. Each request and extension must be documented with a GIF that shows the feature
92
+ working. For example:
93
+
94
+ # [Inlay hint demo](../../inlay_hint.gif)
95
+ DOCS
96
+ end
97
+ end
98
+
99
+ if missing_docs.any?
100
+ warn(<<~WARN)
101
+ The following listeners are missing documentation:
102
+
103
+ #{missing_docs.map { |k, v| "#{k}\n\n#{v.join("\n")}" }.join("\n\n")}
104
+ WARN
105
+
106
+ abort
107
+ end
108
+
109
+ puts "All listeners are documented!"
110
+ end
111
+ end
112
+ end
@@ -23,7 +23,7 @@ module RubyLsp
23
23
 
24
24
  sig { params(source: String, version: Integer, uri: String, encoding: String).void }
25
25
  def initialize(source:, version:, uri:, encoding: Constant::PositionEncodingKind::UTF8)
26
- @cache = T.let({}, T::Hash[Symbol, T.untyped])
26
+ @cache = T.let({}, T::Hash[String, T.untyped])
27
27
  @encoding = T.let(encoding, String)
28
28
  @source = T.let(source, String)
29
29
  @version = T.let(version, Integer)
@@ -40,10 +40,11 @@ module RubyLsp
40
40
  @source == other.source
41
41
  end
42
42
 
43
+ # TODO: remove this method once all nonpositional requests have been migrated to the listener pattern
43
44
  sig do
44
45
  type_parameters(:T)
45
46
  .params(
46
- request_name: Symbol,
47
+ request_name: String,
47
48
  block: T.proc.params(document: Document).returns(T.type_parameter(:T)),
48
49
  ).returns(T.type_parameter(:T))
49
50
  end
@@ -56,6 +57,16 @@ module RubyLsp
56
57
  result
57
58
  end
58
59
 
60
+ sig { type_parameters(:T).params(request_name: String, value: T.type_parameter(:T)).returns(T.type_parameter(:T)) }
61
+ def cache_set(request_name, value)
62
+ @cache[request_name] = value
63
+ end
64
+
65
+ sig { params(request_name: String).returns(T.untyped) }
66
+ def cache_get(request_name)
67
+ @cache[request_name]
68
+ end
69
+
59
70
  sig { params(edits: T::Array[EditShape], version: Integer).void }
60
71
  def push_edits(edits, version:)
61
72
  edits.each do |edit|
@@ -13,42 +13,108 @@ module RubyLsp
13
13
  #
14
14
  # ```ruby
15
15
  # target_node = document.locate_node(position)
16
- # listener = Requests::Hover.new
17
- # EventEmitter.new(listener).emit_for_target(target_node)
16
+ # emitter = EventEmitter.new
17
+ # listener = Requests::Hover.new(emitter, @message_queue)
18
+ # emitter.emit_for_target(target_node)
18
19
  # listener.response
19
20
  # ```
20
21
  class EventEmitter < SyntaxTree::Visitor
21
22
  extend T::Sig
22
23
 
23
- sig { params(listeners: Listener[T.untyped]).void }
24
- def initialize(*listeners)
25
- @listeners = listeners
26
-
27
- # Create a map of event name to listeners that have registered it, so that we avoid unnecessary invocations
28
- @event_to_listener_map = T.let(
29
- listeners.each_with_object(Hash.new { |h, k| h[k] = [] }) do |listener, hash|
30
- listener.class.events&.each { |event| hash[event] << listener }
31
- end,
32
- T::Hash[Symbol, T::Array[Listener[T.untyped]]],
33
- )
34
-
24
+ sig { void }
25
+ def initialize
26
+ @listeners = T.let(Hash.new { |h, k| h[k] = [] }, T::Hash[Symbol, T::Array[Listener[T.untyped]]])
35
27
  super()
36
28
  end
37
29
 
30
+ sig { params(listener: Listener[T.untyped], events: Symbol).void }
31
+ def register(listener, *events)
32
+ events.each { |event| T.must(@listeners[event]) << listener }
33
+ end
34
+
38
35
  # Emit events for a specific node. This is similar to the regular `visit` method, but avoids going deeper into the
39
36
  # tree for performance
40
37
  sig { params(node: T.nilable(SyntaxTree::Node)).void }
41
38
  def emit_for_target(node)
42
39
  case node
43
40
  when SyntaxTree::Command
44
- @event_to_listener_map[:on_command]&.each { |listener| T.unsafe(listener).on_command(node) }
41
+ @listeners[:on_command]&.each { |l| T.unsafe(l).on_command(node) }
45
42
  when SyntaxTree::CallNode
46
- @event_to_listener_map[:on_call]&.each { |listener| T.unsafe(listener).on_call(node) }
43
+ @listeners[:on_call]&.each { |l| T.unsafe(l).on_call(node) }
44
+ when SyntaxTree::TStringContent
45
+ @listeners[:on_tstring_content]&.each { |l| T.unsafe(l).on_tstring_content(node) }
47
46
  when SyntaxTree::ConstPathRef
48
- @event_to_listener_map[:on_const_path_ref]&.each { |listener| T.unsafe(listener).on_const_path_ref(node) }
47
+ @listeners[:on_const_path_ref]&.each { |l| T.unsafe(l).on_const_path_ref(node) }
49
48
  when SyntaxTree::Const
50
- @event_to_listener_map[:on_const]&.each { |listener| T.unsafe(listener).on_const(node) }
49
+ @listeners[:on_const]&.each { |l| T.unsafe(l).on_const(node) }
51
50
  end
52
51
  end
52
+
53
+ # Visit dispatchers are below. Notice that for nodes that create a new scope (e.g.: classes, modules, method defs)
54
+ # we need both an `on_*` and `after_*` event. This is because some requests must know when we exit the scope
55
+ sig { override.params(node: SyntaxTree::ClassDeclaration).void }
56
+ def visit_class(node)
57
+ @listeners[:on_class]&.each { |l| T.unsafe(l).on_class(node) }
58
+ super
59
+ @listeners[:after_class]&.each { |l| T.unsafe(l).after_class(node) }
60
+ end
61
+
62
+ sig { override.params(node: SyntaxTree::ModuleDeclaration).void }
63
+ def visit_module(node)
64
+ @listeners[:on_module]&.each { |l| T.unsafe(l).on_module(node) }
65
+ super
66
+ @listeners[:after_module]&.each { |l| T.unsafe(l).after_module(node) }
67
+ end
68
+
69
+ sig { override.params(node: SyntaxTree::Command).void }
70
+ def visit_command(node)
71
+ @listeners[:on_command]&.each { |l| T.unsafe(l).on_command(node) }
72
+ super
73
+ @listeners[:after_command]&.each { |l| T.unsafe(l).after_command(node) }
74
+ end
75
+
76
+ sig { override.params(node: SyntaxTree::CallNode).void }
77
+ def visit_call(node)
78
+ @listeners[:on_call]&.each { |l| T.unsafe(l).on_call(node) }
79
+ super
80
+ @listeners[:after_call]&.each { |l| T.unsafe(l).after_call(node) }
81
+ end
82
+
83
+ sig { override.params(node: SyntaxTree::VCall).void }
84
+ def visit_vcall(node)
85
+ @listeners[:on_vcall]&.each { |l| T.unsafe(l).on_vcall(node) }
86
+ super
87
+ end
88
+
89
+ sig { override.params(node: SyntaxTree::ConstPathField).void }
90
+ def visit_const_path_field(node)
91
+ @listeners[:on_const_path_field]&.each { |l| T.unsafe(l).on_const_path_field(node) }
92
+ super
93
+ end
94
+
95
+ sig { override.params(node: SyntaxTree::TopConstField).void }
96
+ def visit_top_const_field(node)
97
+ @listeners[:on_top_const_field]&.each { |l| T.unsafe(l).on_top_const_field(node) }
98
+ super
99
+ end
100
+
101
+ sig { override.params(node: SyntaxTree::DefNode).void }
102
+ def visit_def(node)
103
+ @listeners[:on_def]&.each { |l| T.unsafe(l).on_def(node) }
104
+ super
105
+ @listeners[:after_def]&.each { |l| T.unsafe(l).after_def(node) }
106
+ end
107
+
108
+ sig { override.params(node: SyntaxTree::VarField).void }
109
+ def visit_var_field(node)
110
+ @listeners[:on_var_field]&.each { |l| T.unsafe(l).on_var_field(node) }
111
+ super
112
+ end
113
+
114
+ sig { override.params(node: SyntaxTree::Comment).void }
115
+ def visit_comment(node)
116
+ @listeners[:on_comment]&.each { |l| T.unsafe(l).on_comment(node) }
117
+ super
118
+ end
53
119
  end
54
120
  end