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.
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