ruby-lsp 0.4.2 → 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.
- checksums.yaml +4 -4
- data/README.md +29 -116
- data/VERSION +1 -1
- data/exe/ruby-lsp +10 -1
- data/lib/rubocop/cop/ruby_lsp/use_language_server_aliases.rb +62 -0
- data/lib/ruby_lsp/check_docs.rb +112 -0
- data/lib/ruby_lsp/document.rb +87 -8
- data/lib/ruby_lsp/event_emitter.rb +120 -0
- data/lib/ruby_lsp/executor.rb +191 -44
- data/lib/ruby_lsp/extension.rb +104 -0
- data/lib/ruby_lsp/internal.rb +4 -0
- data/lib/ruby_lsp/listener.rb +42 -0
- data/lib/ruby_lsp/requests/base_request.rb +2 -90
- data/lib/ruby_lsp/requests/code_action_resolve.rb +47 -20
- data/lib/ruby_lsp/requests/code_actions.rb +6 -5
- data/lib/ruby_lsp/requests/code_lens.rb +151 -0
- data/lib/ruby_lsp/requests/diagnostics.rb +5 -5
- data/lib/ruby_lsp/requests/document_highlight.rb +8 -10
- data/lib/ruby_lsp/requests/document_link.rb +17 -15
- data/lib/ruby_lsp/requests/document_symbol.rb +63 -40
- data/lib/ruby_lsp/requests/folding_ranges.rb +14 -10
- data/lib/ruby_lsp/requests/formatting.rb +15 -15
- data/lib/ruby_lsp/requests/hover.rb +45 -34
- data/lib/ruby_lsp/requests/inlay_hints.rb +6 -5
- data/lib/ruby_lsp/requests/on_type_formatting.rb +5 -1
- data/lib/ruby_lsp/requests/path_completion.rb +21 -51
- data/lib/ruby_lsp/requests/selection_ranges.rb +4 -4
- data/lib/ruby_lsp/requests/semantic_highlighting.rb +30 -16
- data/lib/ruby_lsp/requests/support/common.rb +91 -0
- data/lib/ruby_lsp/requests/support/highlight_target.rb +5 -4
- data/lib/ruby_lsp/requests/support/rails_document_client.rb +7 -6
- data/lib/ruby_lsp/requests/support/rubocop_diagnostics_runner.rb +0 -1
- data/lib/ruby_lsp/requests/support/rubocop_formatting_runner.rb +0 -1
- data/lib/ruby_lsp/requests/support/selection_range.rb +1 -1
- data/lib/ruby_lsp/requests/support/semantic_token_encoder.rb +2 -2
- data/lib/ruby_lsp/requests/support/sorbet.rb +5 -15
- data/lib/ruby_lsp/requests/support/syntax_tree_formatting_runner.rb +42 -0
- data/lib/ruby_lsp/requests.rb +17 -14
- data/lib/ruby_lsp/server.rb +45 -9
- data/lib/ruby_lsp/store.rb +11 -11
- data/lib/ruby_lsp/utils.rb +9 -8
- metadata +13 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f8f2f2aa1083dcc700e709ebd0bc3d3aed8f08d80943595ba3c9df332d806709
|
4
|
+
data.tar.gz: a065d2f6be6539c81ad117bfba7c7b4f90ead0bbce37dd936733218e3135bc67
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: de010f852e08fd72ac60a12cbb9a8666a92945c49b8a14214f0e347c9a6fbacb65770322627db22a30b573ee61f34ec18be56d625768a6c954cf447dbda63368
|
7
|
+
data.tar.gz: 3f47e62896899091c4a9b166937720cc535f46004827a098bfb7eaf9c31fb39bb35261077b91a01a41baa18a50482d8ffe1b8ebddabaed1ea15bc0d023e76dae
|
data/README.md
CHANGED
@@ -1,148 +1,61 @@
|
|
1
|
-

|
1
|
+
[](https://github.com/Shopify/ruby-lsp/actions/workflows/ci.yml)
|
2
|
+
[](https://marketplace.visualstudio.com/items?itemName=Shopify.ruby-lsp)
|
3
|
+
[](https://join.slack.com/t/ruby-dx/shared_invite/zt-1s6f4y15t-v9jedZ9YUPQLM91TEJ4Gew)
|
2
4
|
|
3
5
|
# Ruby LSP
|
4
6
|
|
5
|
-
|
7
|
+
The Ruby LSP is an implementation of the [language server protocol](https://microsoft.github.io/language-server-protocol/)
|
8
|
+
for Ruby, used to improve rich features in editors. It is a part of a wider goal to provide a state-of-the-art
|
9
|
+
experience to Ruby developers using modern standards for cross-editor features, documentation and debugging.
|
6
10
|
|
7
|
-
|
11
|
+
Want to discuss Ruby developer experience? Consider joining the public
|
12
|
+
[Ruby DX Slack workspace](https://join.slack.com/t/ruby-dx/shared_invite/zt-1s6f4y15t-v9jedZ9YUPQLM91TEJ4Gew).
|
8
13
|
|
9
|
-
|
10
|
-
|
11
|
-
It's part of a [wider Shopify goal](https://github.com/Shopify/vscode-shopify-ruby) to provide a state-of-the-art experience to Ruby developers using modern standards for cross-editor features, documentation and debugging.
|
12
|
-
|
13
|
-
It provides many features, including:
|
14
|
-
|
15
|
-
* Syntax highlighting
|
16
|
-
* Linting and formatting
|
17
|
-
* Code folding
|
18
|
-
* Selection ranges
|
19
|
-
|
20
|
-
It does not perform typechecking, so its features are implemented on a best-effort basis, aiming to be as accurate as possible.
|
21
|
-
|
22
|
-
Planned future features include:
|
23
|
-
|
24
|
-
* Auto-completion and navigation ("Go To Definition") ([prototype](https://github.com/Shopify/ruby-lsp/pull/429))
|
25
|
-
* Support for plug-ins to extend behavior
|
26
|
-
|
27
|
-
The Ruby LSP does not perform any type-checking or provide any type-related assistance, but it can be used alongside [Sorbet](https://github.com/sorbet/sorbet)'s LSP server.
|
14
|
+
## Usage
|
28
15
|
|
29
|
-
|
16
|
+
### With VS Code
|
30
17
|
|
31
|
-
|
32
|
-
|
18
|
+
If using VS Code, all you have to do is install the [Ruby LSP extension](https://github.com/Shopify/vscode-ruby-lsp) to
|
19
|
+
get the extra features in the editor. Do not install this gem manually.
|
33
20
|
|
34
|
-
|
35
|
-
|
36
|
-
* [RubyConf 2022: Improving the development experience with language servers](https://www.youtube.com/watch?v=kEfXPTm1aCI) ([Vinicius Stock](https://github.com/vinistock))
|
21
|
+
### With other editors
|
37
22
|
|
38
|
-
|
23
|
+
See [editors](EDITORS.md) for community instructions on setting up the Ruby LSP.
|
39
24
|
|
40
|
-
|
25
|
+
The gem can be installed by doing
|
26
|
+
```shell
|
27
|
+
gem install ruby-lsp
|
28
|
+
```
|
41
29
|
|
30
|
+
If you decide to add the gem to the bundle, it is not necessary to require it.
|
42
31
|
```ruby
|
43
32
|
group :development do
|
44
33
|
gem "ruby-lsp", require: false
|
45
34
|
end
|
46
35
|
```
|
47
36
|
|
48
|
-
|
49
|
-
in the editor. See [editors](https://github.com/Shopify/ruby-lsp/blob/main/EDITORS.md) for community instructions on
|
50
|
-
setting up the Ruby LSP in different editors.
|
37
|
+
### Documentation
|
51
38
|
|
52
39
|
See the [documentation](https://shopify.github.io/ruby-lsp) for more in-depth details about the
|
53
40
|
[supported features](https://shopify.github.io/ruby-lsp/RubyLsp/Requests.html).
|
54
41
|
|
42
|
+
## Learn More
|
43
|
+
|
44
|
+
* [RubyConf 2022: Improving the development experience with language servers](https://www.youtube.com/watch?v=kEfXPTm1aCI) ([Vinicius Stock](https://github.com/vinistock))
|
45
|
+
* [Remote Ruby: Ruby Language Server with Vinicius Stock](https://remoteruby.com/221)
|
46
|
+
|
55
47
|
## Contributing
|
56
48
|
|
57
49
|
Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/ruby-lsp.
|
58
50
|
This project is intended to be a safe, welcoming space for collaboration, and contributors
|
59
51
|
are expected to adhere to the
|
60
|
-
[Contributor Covenant](
|
52
|
+
[Contributor Covenant](CODE_OF_CONDUCT.md)
|
61
53
|
code of conduct.
|
62
54
|
|
63
|
-
|
64
|
-
|
65
|
-
Run the test suite with `bin/test`.
|
66
|
-
|
67
|
-
For more visibility into which tests are running, use the `SpecReporter`:
|
68
|
-
|
69
|
-
`SPEC_REPORTER=1 bin/test`
|
70
|
-
|
71
|
-
By default the tests run with warnings disabled to reduce noise. To enable warnings, pass `VERBOSE=1`.
|
72
|
-
Warnings are always shown when running in CI.
|
73
|
-
|
74
|
-
### Expectation testing
|
75
|
-
|
76
|
-
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.
|
77
|
-
|
78
|
-
We define expectations as `.exp` files, of which there are two variants:
|
79
|
-
* `.exp.rb`, to indicate the resulting code after an operation.
|
80
|
-
* `.exp.json`, consisting of a `result`, and an optional set of input `params`.
|
81
|
-
|
82
|
-
To add a new fixture to the expectations test suite:
|
83
|
-
|
84
|
-
1. Add a new fixture `my_fixture.rb` file under `test/fixtures`
|
85
|
-
2. (optional) Add new expectations under `test/expectations/$HANDLER` for the request handlers you're concerned by
|
86
|
-
3. Profit by running `bin/test test/requests/$HANDLER_expectations_test my_fixture`
|
87
|
-
* Handlers for which you added expectations will be checked with `assert_expectations`
|
88
|
-
* Handlers without expectations will be ran against your new test to check that nothing breaks
|
89
|
-
|
90
|
-
To add a new expectations test runner for a new request handler:
|
91
|
-
|
92
|
-
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.
|
93
|
-
|
94
|
-
```rb
|
95
|
-
# frozen_string_literal: true
|
96
|
-
|
97
|
-
require "test_helper"
|
98
|
-
require "expectations/expectations_test_runner"
|
99
|
-
|
100
|
-
class $HANDLERExpectationsTest < ExpectationsTestRunner
|
101
|
-
expectations_tests RubyLsp::Requests::$HANDLER, "$EXPECTATIONS_DIR"
|
102
|
-
end
|
103
|
-
```
|
104
|
-
|
105
|
-
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.
|
106
|
-
|
107
|
-
4. (optional) Add new fixtures for your handler under `test/fixtures`
|
108
|
-
|
109
|
-
5. (optional) Add new expectations under `test/expectations/$HANDLER`
|
110
|
-
* No need to write the expectations by hand, just run the test with an empty expectation file and copy from the output.
|
111
|
-
|
112
|
-
7. Profit by running, `bin/test test/expectations_test $HANDLER`
|
113
|
-
* Tests with expectations will be checked with `assert_expectations`
|
114
|
-
* Tests without expectations will be ran against your new $HANDLER to check that nothing breaks
|
115
|
-
|
116
|
-
## Debugging
|
117
|
-
|
118
|
-
### Debugging Tests
|
119
|
-
|
120
|
-
1. Open the test file.
|
121
|
-
2. Set a breakpoint(s) on lines by clicking next to their numbers.
|
122
|
-
3. Open VS Code's `Run and Debug` panel.
|
123
|
-
4. At the top of the panel, select `Minitset - current file` and click the green triangle (or press F5).
|
124
|
-
|
125
|
-
### Debugging Running Ruby LSP Process
|
126
|
-
|
127
|
-
1. Open the `vscode-ruby-lsp` project in VS Code.
|
128
|
-
2. [`vscode-ruby-lsp`] Open VS Code's `Run and Debug` panel.
|
129
|
-
3. [`vscode-ruby-lsp`] Select `Run Extension` and click the green triangle (or press F5).
|
130
|
-
4. [`vscode-ruby-lsp`] Now VS Code will:
|
131
|
-
- Open another workspace as the `Extension Development Host`.
|
132
|
-
- Run `vscode-ruby-lsp` extension in debug mode, which will start a new `ruby-lsp` process with the `--debug` flag.
|
133
|
-
5. Open `ruby-lsp` in VS Code.
|
134
|
-
6. [`ruby-lsp`] Run `bin/rdbg -A` to connect to the running `ruby-lsp` process.
|
135
|
-
7. [`ruby-lsp`] Use commands like `b <file>:<line>` or `b Class#method` to set breakpoints and type `c` to continue the process.
|
136
|
-
8. In your `Extension Development Host` project (e.g. [`Tapioca`](https://github.com/Shopify/tapioca)), trigger the request that will hit the breakpoint.
|
137
|
-
|
138
|
-
## Spell Checking
|
139
|
-
|
140
|
-
VS Code users will be prompted to enable the [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) extension.
|
141
|
-
By default this will be enabled for all workspaces, but you can choose to selectively enable or disable it per workspace.
|
142
|
-
|
143
|
-
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.
|
144
57
|
|
145
58
|
## License
|
146
59
|
|
147
60
|
The gem is available as open source under the terms of the
|
148
|
-
[MIT License](
|
61
|
+
[MIT License](LICENSE.txt).
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.5.0
|
data/exe/ruby-lsp
CHANGED
@@ -21,6 +21,11 @@ end
|
|
21
21
|
require_relative "../lib/ruby_lsp/internal"
|
22
22
|
|
23
23
|
if ARGV.include?("--debug")
|
24
|
+
if ["x64-mingw-ucrt", "x64-mingw32"].include?(RUBY_PLATFORM)
|
25
|
+
puts "Debugging is not supported on Windows"
|
26
|
+
exit 1
|
27
|
+
end
|
28
|
+
|
24
29
|
sockets_dir = "/tmp/ruby-lsp-debug-sockets"
|
25
30
|
Dir.mkdir(sockets_dir) unless Dir.exist?(sockets_dir)
|
26
31
|
# ruby-debug-ENV["USER"] is an implicit naming pattern in ruby/debug
|
@@ -28,7 +33,11 @@ if ARGV.include?("--debug")
|
|
28
33
|
socket_identifier = "ruby-debug-#{ENV["USER"]}-#{File.basename(Dir.pwd)}.sock"
|
29
34
|
ENV["RUBY_DEBUG_SOCK_PATH"] = "#{sockets_dir}/#{socket_identifier}"
|
30
35
|
|
31
|
-
|
36
|
+
begin
|
37
|
+
require "debug/open_nonstop"
|
38
|
+
rescue LoadError
|
39
|
+
warn("You need to install the debug gem to use the --debug flag")
|
40
|
+
end
|
32
41
|
end
|
33
42
|
|
34
43
|
RubyLsp::Server.new.start
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "rubocop"
|
5
|
+
require "sorbet-runtime"
|
6
|
+
|
7
|
+
module RuboCop
|
8
|
+
module Cop
|
9
|
+
module RubyLsp
|
10
|
+
# Prefer using `Interface`, `Transport` and `Constant` aliases
|
11
|
+
# within the `RubyLsp` module, without having to prefix with
|
12
|
+
# `LanguageServer::Protocol`
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# # bad
|
16
|
+
# module RubyLsp
|
17
|
+
# class FoldingRanges
|
18
|
+
# sig { override.returns(T.all(T::Array[LanguageServer::Protocol::Interface::FoldingRange], Object)) }
|
19
|
+
# def run; end
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# # good
|
23
|
+
# module RubyLsp
|
24
|
+
# class FoldingRanges
|
25
|
+
# sig { override.returns(T.all(T::Array[Interface::FoldingRange], Object)) }
|
26
|
+
# def run; end
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
class UseLanguageServerAliases < RuboCop::Cop::Base
|
30
|
+
extend RuboCop::Cop::AutoCorrector
|
31
|
+
|
32
|
+
ALIASED_CONSTANTS = T.let([:Interface, :Transport, :Constant].freeze, T::Array[Symbol])
|
33
|
+
|
34
|
+
MSG = "Use constant alias `%{constant}`."
|
35
|
+
|
36
|
+
def_node_search :ruby_lsp_modules, <<~PATTERN
|
37
|
+
(module (const nil? :RubyLsp) ...)
|
38
|
+
PATTERN
|
39
|
+
|
40
|
+
def_node_search :lsp_constant_usages, <<~PATTERN
|
41
|
+
(const (const (const nil? :LanguageServer) :Protocol) {:Interface | :Transport | :Constant})
|
42
|
+
PATTERN
|
43
|
+
|
44
|
+
def on_new_investigation
|
45
|
+
return if processed_source.blank?
|
46
|
+
|
47
|
+
ruby_lsp_modules(processed_source.ast).each do |ruby_lsp_mod|
|
48
|
+
lsp_constant_usages(ruby_lsp_mod).each do |node|
|
49
|
+
lsp_const = node.children.last
|
50
|
+
|
51
|
+
next unless ALIASED_CONSTANTS.include?(lsp_const)
|
52
|
+
|
53
|
+
add_offense(node, message: format(MSG, constant: lsp_const)) do |corrector|
|
54
|
+
corrector.replace(node, lsp_const)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -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
|
data/lib/ruby_lsp/document.rb
CHANGED
@@ -15,11 +15,19 @@ module RubyLsp
|
|
15
15
|
sig { returns(String) }
|
16
16
|
attr_reader :source
|
17
17
|
|
18
|
-
sig {
|
19
|
-
|
20
|
-
|
18
|
+
sig { returns(Integer) }
|
19
|
+
attr_reader :version
|
20
|
+
|
21
|
+
sig { returns(String) }
|
22
|
+
attr_reader :uri
|
23
|
+
|
24
|
+
sig { params(source: String, version: Integer, uri: String, encoding: String).void }
|
25
|
+
def initialize(source:, version:, uri:, encoding: Constant::PositionEncodingKind::UTF8)
|
26
|
+
@cache = T.let({}, T::Hash[String, T.untyped])
|
21
27
|
@encoding = T.let(encoding, String)
|
22
28
|
@source = T.let(source, String)
|
29
|
+
@version = T.let(version, Integer)
|
30
|
+
@uri = T.let(uri, String)
|
23
31
|
@unparsed_edits = T.let([], T::Array[EditShape])
|
24
32
|
@syntax_error = T.let(false, T::Boolean)
|
25
33
|
@tree = T.let(SyntaxTree.parse(@source), T.nilable(SyntaxTree::Node))
|
@@ -32,10 +40,11 @@ module RubyLsp
|
|
32
40
|
@source == other.source
|
33
41
|
end
|
34
42
|
|
43
|
+
# TODO: remove this method once all nonpositional requests have been migrated to the listener pattern
|
35
44
|
sig do
|
36
45
|
type_parameters(:T)
|
37
46
|
.params(
|
38
|
-
request_name:
|
47
|
+
request_name: String,
|
39
48
|
block: T.proc.params(document: Document).returns(T.type_parameter(:T)),
|
40
49
|
).returns(T.type_parameter(:T))
|
41
50
|
end
|
@@ -48,8 +57,18 @@ module RubyLsp
|
|
48
57
|
result
|
49
58
|
end
|
50
59
|
|
51
|
-
sig { params(
|
52
|
-
def
|
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
|
+
|
70
|
+
sig { params(edits: T::Array[EditShape], version: Integer).void }
|
71
|
+
def push_edits(edits, version:)
|
53
72
|
edits.each do |edit|
|
54
73
|
range = edit[:range]
|
55
74
|
scanner = create_scanner
|
@@ -60,6 +79,7 @@ module RubyLsp
|
|
60
79
|
@source[start_position...end_position] = edit[:text]
|
61
80
|
end
|
62
81
|
|
82
|
+
@version = version
|
63
83
|
@unparsed_edits.concat(edits)
|
64
84
|
@cache.clear
|
65
85
|
end
|
@@ -68,9 +88,9 @@ module RubyLsp
|
|
68
88
|
def parse
|
69
89
|
return if @unparsed_edits.empty?
|
70
90
|
|
91
|
+
@unparsed_edits.clear
|
71
92
|
@tree = SyntaxTree.parse(@source)
|
72
93
|
@syntax_error = false
|
73
|
-
@unparsed_edits.clear
|
74
94
|
rescue SyntaxTree::Parser::ParseError
|
75
95
|
@syntax_error = true
|
76
96
|
end
|
@@ -90,6 +110,61 @@ module RubyLsp
|
|
90
110
|
Scanner.new(@source, @encoding)
|
91
111
|
end
|
92
112
|
|
113
|
+
sig do
|
114
|
+
params(
|
115
|
+
position: PositionShape,
|
116
|
+
node_types: T::Array[T.class_of(SyntaxTree::Node)],
|
117
|
+
).returns([T.nilable(SyntaxTree::Node), T.nilable(SyntaxTree::Node)])
|
118
|
+
end
|
119
|
+
def locate_node(position, node_types: [])
|
120
|
+
return [nil, nil] unless parsed?
|
121
|
+
|
122
|
+
locate(T.must(@tree), create_scanner.find_char_position(position))
|
123
|
+
end
|
124
|
+
|
125
|
+
sig do
|
126
|
+
params(
|
127
|
+
node: SyntaxTree::Node,
|
128
|
+
char_position: Integer,
|
129
|
+
node_types: T::Array[T.class_of(SyntaxTree::Node)],
|
130
|
+
).returns([T.nilable(SyntaxTree::Node), T.nilable(SyntaxTree::Node)])
|
131
|
+
end
|
132
|
+
def locate(node, char_position, node_types: [])
|
133
|
+
queue = T.let(node.child_nodes.compact, T::Array[T.nilable(SyntaxTree::Node)])
|
134
|
+
closest = node
|
135
|
+
parent = T.let(nil, T.nilable(SyntaxTree::Node))
|
136
|
+
|
137
|
+
until queue.empty?
|
138
|
+
candidate = queue.shift
|
139
|
+
|
140
|
+
# Skip nil child nodes
|
141
|
+
next if candidate.nil?
|
142
|
+
|
143
|
+
# Add the next child_nodes to the queue to be processed
|
144
|
+
queue.concat(candidate.child_nodes)
|
145
|
+
|
146
|
+
# Skip if the current node doesn't cover the desired position
|
147
|
+
loc = candidate.location
|
148
|
+
next unless (loc.start_char...loc.end_char).cover?(char_position)
|
149
|
+
|
150
|
+
# If the node's start character is already past the position, then we should've found the closest node
|
151
|
+
# already
|
152
|
+
break if char_position < loc.start_char
|
153
|
+
|
154
|
+
# If there are node types to filter by, and the current node is not one of those types, then skip it
|
155
|
+
next if node_types.any? && node_types.none? { |type| candidate.class == type }
|
156
|
+
|
157
|
+
# If the current node is narrower than or equal to the previous closest node, then it is more precise
|
158
|
+
closest_loc = closest.location
|
159
|
+
if loc.end_char - loc.start_char <= closest_loc.end_char - closest_loc.start_char
|
160
|
+
parent = closest
|
161
|
+
closest = candidate
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
[closest, parent]
|
166
|
+
end
|
167
|
+
|
93
168
|
class Scanner
|
94
169
|
extend T::Sig
|
95
170
|
|
@@ -118,7 +193,11 @@ module RubyLsp
|
|
118
193
|
# The final position is the beginning of the line plus the requested column. If the encoding is UTF-16, we also
|
119
194
|
# need to adjust for surrogate pairs
|
120
195
|
requested_position = @pos + position[:character]
|
121
|
-
|
196
|
+
|
197
|
+
if @encoding == Constant::PositionEncodingKind::UTF16
|
198
|
+
requested_position -= utf_16_character_position_correction(@pos, requested_position)
|
199
|
+
end
|
200
|
+
|
122
201
|
requested_position
|
123
202
|
end
|
124
203
|
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module RubyLsp
|
5
|
+
# EventEmitter is an intermediary between our requests and Syntax Tree visitors. It's used to visit the document's AST
|
6
|
+
# and emit events that the requests can listen to for providing functionality. Usages:
|
7
|
+
#
|
8
|
+
# - For positional requests, locate the target node and use `emit_for_target` to fire events for each listener
|
9
|
+
# - For nonpositional requests, use `visit` to go through the AST, which will fire events for each listener as nodes
|
10
|
+
# are found
|
11
|
+
#
|
12
|
+
# # Example
|
13
|
+
#
|
14
|
+
# ```ruby
|
15
|
+
# target_node = document.locate_node(position)
|
16
|
+
# emitter = EventEmitter.new
|
17
|
+
# listener = Requests::Hover.new(emitter, @message_queue)
|
18
|
+
# emitter.emit_for_target(target_node)
|
19
|
+
# listener.response
|
20
|
+
# ```
|
21
|
+
class EventEmitter < SyntaxTree::Visitor
|
22
|
+
extend T::Sig
|
23
|
+
|
24
|
+
sig { void }
|
25
|
+
def initialize
|
26
|
+
@listeners = T.let(Hash.new { |h, k| h[k] = [] }, T::Hash[Symbol, T::Array[Listener[T.untyped]]])
|
27
|
+
super()
|
28
|
+
end
|
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
|
+
|
35
|
+
# Emit events for a specific node. This is similar to the regular `visit` method, but avoids going deeper into the
|
36
|
+
# tree for performance
|
37
|
+
sig { params(node: T.nilable(SyntaxTree::Node)).void }
|
38
|
+
def emit_for_target(node)
|
39
|
+
case node
|
40
|
+
when SyntaxTree::Command
|
41
|
+
@listeners[:on_command]&.each { |l| T.unsafe(l).on_command(node) }
|
42
|
+
when SyntaxTree::CallNode
|
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) }
|
46
|
+
when SyntaxTree::ConstPathRef
|
47
|
+
@listeners[:on_const_path_ref]&.each { |l| T.unsafe(l).on_const_path_ref(node) }
|
48
|
+
when SyntaxTree::Const
|
49
|
+
@listeners[:on_const]&.each { |l| T.unsafe(l).on_const(node) }
|
50
|
+
end
|
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
|
119
|
+
end
|
120
|
+
end
|