ruby-lsp 0.4.2 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +29 -116
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp +10 -1
  5. data/lib/rubocop/cop/ruby_lsp/use_language_server_aliases.rb +62 -0
  6. data/lib/ruby_lsp/check_docs.rb +112 -0
  7. data/lib/ruby_lsp/document.rb +87 -8
  8. data/lib/ruby_lsp/event_emitter.rb +120 -0
  9. data/lib/ruby_lsp/executor.rb +191 -44
  10. data/lib/ruby_lsp/extension.rb +104 -0
  11. data/lib/ruby_lsp/internal.rb +4 -0
  12. data/lib/ruby_lsp/listener.rb +42 -0
  13. data/lib/ruby_lsp/requests/base_request.rb +2 -90
  14. data/lib/ruby_lsp/requests/code_action_resolve.rb +47 -20
  15. data/lib/ruby_lsp/requests/code_actions.rb +6 -5
  16. data/lib/ruby_lsp/requests/code_lens.rb +151 -0
  17. data/lib/ruby_lsp/requests/diagnostics.rb +5 -5
  18. data/lib/ruby_lsp/requests/document_highlight.rb +8 -10
  19. data/lib/ruby_lsp/requests/document_link.rb +17 -15
  20. data/lib/ruby_lsp/requests/document_symbol.rb +63 -40
  21. data/lib/ruby_lsp/requests/folding_ranges.rb +14 -10
  22. data/lib/ruby_lsp/requests/formatting.rb +15 -15
  23. data/lib/ruby_lsp/requests/hover.rb +45 -34
  24. data/lib/ruby_lsp/requests/inlay_hints.rb +6 -5
  25. data/lib/ruby_lsp/requests/on_type_formatting.rb +5 -1
  26. data/lib/ruby_lsp/requests/path_completion.rb +21 -51
  27. data/lib/ruby_lsp/requests/selection_ranges.rb +4 -4
  28. data/lib/ruby_lsp/requests/semantic_highlighting.rb +30 -16
  29. data/lib/ruby_lsp/requests/support/common.rb +91 -0
  30. data/lib/ruby_lsp/requests/support/highlight_target.rb +5 -4
  31. data/lib/ruby_lsp/requests/support/rails_document_client.rb +7 -6
  32. data/lib/ruby_lsp/requests/support/rubocop_diagnostics_runner.rb +0 -1
  33. data/lib/ruby_lsp/requests/support/rubocop_formatting_runner.rb +0 -1
  34. data/lib/ruby_lsp/requests/support/selection_range.rb +1 -1
  35. data/lib/ruby_lsp/requests/support/semantic_token_encoder.rb +2 -2
  36. data/lib/ruby_lsp/requests/support/sorbet.rb +5 -15
  37. data/lib/ruby_lsp/requests/support/syntax_tree_formatting_runner.rb +42 -0
  38. data/lib/ruby_lsp/requests.rb +17 -14
  39. data/lib/ruby_lsp/server.rb +45 -9
  40. data/lib/ruby_lsp/store.rb +11 -11
  41. data/lib/ruby_lsp/utils.rb +9 -8
  42. metadata +13 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c5d9c6b487aab55ce25eb2741fff92afa8ca0ee544f21374debacce7f887cc50
4
- data.tar.gz: 8da9da85900753306f5e8558bc1a42f25b7173ffe1087ea70612c10ef5558e27
3
+ metadata.gz: f8f2f2aa1083dcc700e709ebd0bc3d3aed8f08d80943595ba3c9df332d806709
4
+ data.tar.gz: a065d2f6be6539c81ad117bfba7c7b4f90ead0bbce37dd936733218e3135bc67
5
5
  SHA512:
6
- metadata.gz: 3dca630b9cae262f1af8d0aef0300ab7399dafd348db5f578ed7fdffb22a7afd48fe9467aa65e467fb50e16663f8fad32f1d0cc46b30819b06a6b7472700dcc0
7
- data.tar.gz: f048c6c9fe14d20edefeca7ac504942af34c61699163f0d09ec54f4d80e7469e1bb1762757b96aa3d8d06a6e8168473dacfc932112b2d31ca4b380748eef8899
6
+ metadata.gz: de010f852e08fd72ac60a12cbb9a8666a92945c49b8a14214f0e347c9a6fbacb65770322627db22a30b573ee61f34ec18be56d625768a6c954cf447dbda63368
7
+ data.tar.gz: 3f47e62896899091c4a9b166937720cc535f46004827a098bfb7eaf9c31fb39bb35261077b91a01a41baa18a50482d8ffe1b8ebddabaed1ea15bc0d023e76dae
data/README.md CHANGED
@@ -1,148 +1,61 @@
1
- ![Build Status](https://github.com/Shopify/ruby-lsp/workflows/CI/badge.svg)
1
+ [![Build Status](https://github.com/Shopify/ruby-lsp/workflows/CI/badge.svg)](https://github.com/Shopify/ruby-lsp/actions/workflows/ci.yml)
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
+ [![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)
2
4
 
3
5
  # Ruby LSP
4
6
 
5
- This gem is an implementation of the [language server protocol specification](https://microsoft.github.io/language-server-protocol/) for Ruby, used to improve editor features.
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
- # Overview
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
- The intention of Ruby LSP is to provide a fast, robust and feature-rich coding environment for Ruby developers.
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
- At the time of writing, these are the major differences between Ruby LSP and [Solargraph](https://solargraph.org/):
16
+ ### With VS Code
30
17
 
31
- * Solargraph [uses](https://solargraph.org/guides/yard) YARD documentation to gather information about your project and its gem dependencies. This provides functionality such as context-aware auto-completion and navigation ("Go To Definition")
32
- * Solargraph can be used as a globally installed gem, but Ruby LSP must be added to the Gemfile or gemspec if using RuboCop. (There are pros and cons to each approach)
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
- ## Learn More
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
- ## Usage
23
+ See [editors](EDITORS.md) for community instructions on setting up the Ruby LSP.
39
24
 
40
- Install the gem. There's no need to require it, since the server is used as a standalone executable.
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
- If using VS Code, install the [Ruby LSP extension](https://github.com/Shopify/vscode-ruby-lsp) to get the extra features
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](https://github.com/Shopify/ruby-lsp/blob/main/CODE_OF_CONDUCT.md)
52
+ [Contributor Covenant](CODE_OF_CONDUCT.md)
61
53
  code of conduct.
62
54
 
63
- ### Running the test suite
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](https://github.com/Shopify/ruby-lsp/blob/main/LICENSE.txt).
61
+ [MIT License](LICENSE.txt).
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.2
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
- require "debug/open_nonstop"
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
@@ -15,11 +15,19 @@ module RubyLsp
15
15
  sig { returns(String) }
16
16
  attr_reader :source
17
17
 
18
- sig { params(source: String, encoding: String).void }
19
- def initialize(source, encoding = "utf-8")
20
- @cache = T.let({}, T::Hash[Symbol, T.untyped])
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: Symbol,
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(edits: T::Array[EditShape]).void }
52
- def push_edits(edits)
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
- requested_position -= utf_16_character_position_correction(@pos, requested_position) if @encoding == "utf-16"
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