ruby-lsp-extras 0.1.1 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e2245486ea8620c7e791732dfe10b35959f2423b61a24379d3285b1a7fdf4a10
4
- data.tar.gz: caad46b7af10f6c480be0a02ce4587c02de59dfaae58cf284a3689f0c41ea95f
3
+ metadata.gz: c8efd636d699d2b97731469135f307359822327a3f6277ebc04f58868b531c93
4
+ data.tar.gz: 2d05242a2ed15aa74f96bbc0cce194126a3dac0b28515877042b47bb0ef0c757
5
5
  SHA512:
6
- metadata.gz: 5c72e4b84314738fc3a848db19c0633bedacb00afe2307059b04f9f2a2c10bb89d61399361a24295c32e0ae79c5b5afca7c01a9a192f1ca7761b6307d4ed07bd
7
- data.tar.gz: 26e145c025bc6d0113b7b97847678e0b13019f5587315bf89c9fe592cf6bb3ef700c7683cbd816179304e9257f38e5d5912521fcc63a8560afa37e83a62364ef
6
+ metadata.gz: 99f0c383f3371b462454512643c1b6677d58a643587f13aa76e3d85b51459116a41acfb3b0630da384411089a8d6cc8438e73e8e8bd85cb760fec7bdb6ce60cf
7
+ data.tar.gz: 6822f14538bddac7bbf72b0fa0563e0049853176bd16ee9debdbafba736e2b57fb91d1bea84fead5c3f944f854de55b4a33288c758d7408773e457bba5068201
data/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## Unreleased changes
4
4
 
5
+ ## 0.2.0
6
+
7
+ - Initial Release
8
+
5
9
  ## 0.1.1
6
10
 
7
11
  - Initial Setup
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ruby LSP add-on (auto-discovered by Ruby LSP; not loaded by Rails).
4
+ #
5
+ # Adds go-to-definition targets Ruby LSP/ruby-lsp-rails don't provide:
6
+ # - Route helpers -> the routed controller action, alongside the route file location ruby-lsp-rails already provides.
7
+ # e.g. `companies_path` -> `CompaniesController#create`
8
+ # - Route declarations in `config/routes.rb`/`config/routes/*.rb` -> the controller action (single-action lines) or
9
+ # controller class (multi-action lines). e.g. `resources :posts` -> `PostsController`
10
+ # - View helpers (`app/helpers/`) called from controllers or views, when the method isn't on the current class.
11
+ # e.g. `my_post_helper_methd` / `helpers.my_post_helper_methd` -> `PostsHelper#my_post_helper_methd`
12
+ # - Column attribute methods in models -> the column's line in `db/schema.rb`. e.g. `title` -> `t.string "title"`
13
+ #
14
+ # Relies on ruby-lsp-rails internals (`rails_runner_client`, server add-ons), so it's pinned to 0.4.x.
15
+ #
16
+ # NOTE: ActiveSupport isn't loaded in the Ruby LSP process -- stick to plain Ruby (no `presence`/`blank?`/`camelize`)
17
+
18
+ require_relative 'definition'
19
+ require_relative 'version'
20
+
21
+ module RubyLsp
22
+ module Extras
23
+ RAILS_ADDON_NAME = 'Ruby LSP Rails'
24
+ RAILS_ADDON_VERSIONS = ['>= 0.4.0', '< 0.5.0'].freeze
25
+ # Must match `ServerAddon#name` (server_addon.rb runs in the Rails process, so it can't share this constant)
26
+ SERVER_ADDON_NAME = 'Extras'
27
+
28
+ # :nodoc:
29
+ class Addon < ::RubyLsp::Addon
30
+ # @see super
31
+ def activate(global_state, outgoing_queue)
32
+ @global_state = global_state
33
+ @outgoing_queue = outgoing_queue
34
+ @client = nil
35
+
36
+ # `rails_runner_client` blocks until the Rails process finishes booting
37
+ @boot_thread = Thread.new { register_server_addon }
38
+ end
39
+
40
+ # @see super
41
+ def deactivate
42
+ @boot_thread&.kill
43
+ end
44
+
45
+ # @see super
46
+ def name
47
+ 'Ruby LSP Extras'
48
+ end
49
+
50
+ # @see super
51
+ def version
52
+ VERSION.to_s
53
+ end
54
+
55
+ # @see super
56
+ def create_definition_listener(response_builder, uri, node_context, dispatcher)
57
+ return if @global_state.nil?
58
+
59
+ Definition.new(@client, response_builder, uri, node_context, @global_state.index, dispatcher: dispatcher)
60
+ end
61
+
62
+ private
63
+
64
+ # @return [void]
65
+ def register_server_addon
66
+ rails_addon = ::RubyLsp::Addon.get(RAILS_ADDON_NAME, *RAILS_ADDON_VERSIONS)
67
+ client = rails_addon.rails_runner_client
68
+ client.register_server_addon(File.expand_path('server_addon.rb', __dir__))
69
+ @client = client
70
+ rescue ::RubyLsp::Addon::AddonNotFoundError, ::RubyLsp::Addon::IncompatibleApiError => e
71
+ log("#{name} add-on disabled: #{e.message}")
72
+ end
73
+
74
+ # @param message [String]
75
+ #
76
+ # @return [void]
77
+ #
78
+ def log(message)
79
+ @outgoing_queue << Notification.window_log_message(message)
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,287 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLsp
4
+ module Extras
5
+ # Adds definitions for route helpers/declarations, view helpers, and model columns
6
+ class Definition
7
+ ROUTE_HELPER = /_(?:path|url)\z/
8
+ # Receivers that proxy to the view context, e.g. `helpers.my_helper` in a controller
9
+ HELPER_PROXIES = %w[helpers view_context].freeze
10
+ HELPERS_DIR = '/app/helpers/'
11
+ # ActiveRecord attribute methods -> column name, e.g. `title=`, `title?`, `title_was`, `saved_change_to_title?`
12
+ ATTRIBUTE_METHOD = /
13
+ \A
14
+ (?:saved_change_to_|will_save_change_to_)?
15
+ (?<column>\w+?)
16
+ (?:=|\?|_was|_changed\?|_before_last_save|_in_database)?
17
+ \z
18
+ /x
19
+
20
+ include ::RubyLsp::Requests::Support::Common
21
+
22
+ # @param client [RubyLsp::Rails::RunnerClient, nil] `nil` until the Rails server finishes booting
23
+ # @param response_builder [RubyLsp::ResponseBuilders::CollectionResponseBuilder]
24
+ # @param uri [URI::Generic] the document the request came from
25
+ # @param node_context [RubyLsp::NodeContext]
26
+ # @param index [RubyIndexer::Index]
27
+ # @param dispatcher [Prism::Dispatcher]
28
+ #
29
+ def initialize(client, response_builder, uri, node_context, index, dispatcher:)
30
+ @client = client
31
+ @response_builder = response_builder
32
+ @path = uri.to_s
33
+ @nesting = node_context.nesting
34
+ @index = index
35
+
36
+ dispatcher.register(self, :on_call_node_enter, :on_symbol_node_enter, :on_string_node_enter)
37
+ end
38
+
39
+ # e.g. `:posts` in `resources :posts`, or `:download_attachment` in `get '...', action: :download_attachment`
40
+ #
41
+ # @param node [Prism::SymbolNode]
42
+ #
43
+ # @return [void]
44
+ #
45
+ def on_symbol_node_enter(node)
46
+ handle_route_declaration(node)
47
+ end
48
+
49
+ # e.g. `'static_pages#test_service'` in `get 'test_service', to: 'static_pages#test_service'`
50
+ #
51
+ # @param node [Prism::StringNode]
52
+ #
53
+ # @return [void]
54
+ #
55
+ def on_string_node_enter(node)
56
+ handle_route_declaration(node)
57
+ end
58
+
59
+ # @param node [Prism::CallNode]
60
+ #
61
+ # @return [void]
62
+ #
63
+ def on_call_node_enter(node)
64
+ message = node.message
65
+ return if message.nil?
66
+
67
+ if self_receiver?(node) && message.match?(ROUTE_HELPER)
68
+ handle_route(message)
69
+ elsif self_receiver?(node) && in_model?
70
+ handle_column(message)
71
+ elsif self_receiver?(node) || proxy_receiver?(node)
72
+ handle_helper(node, message)
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ # @param message [String] e.g. `companies_path`
79
+ #
80
+ # @return [void]
81
+ #
82
+ def handle_route(message)
83
+ return if @client.nil?
84
+
85
+ result = @client.delegate_request(
86
+ server_addon_name: SERVER_ADDON_NAME,
87
+ request_name: 'controller_action',
88
+ name: message,
89
+ )
90
+ return if result.nil?
91
+
92
+ add_route_locations(result[:controller], result[:action])
93
+ end
94
+
95
+ # In a routes file, jumps to whatever the routes declared on this line point at: the action for single-action
96
+ # lines (`get 'x', to: 'foo#bar'`), the controller class for multi-action lines (`resources :posts`)
97
+ #
98
+ # @param node [Prism::SymbolNode, Prism::StringNode]
99
+ #
100
+ # @return [void]
101
+ #
102
+ def handle_route_declaration(node)
103
+ return if @client.nil? || !in_routes_file?
104
+
105
+ result = @client.delegate_request(
106
+ server_addon_name: SERVER_ADDON_NAME,
107
+ request_name: 'routes_at',
108
+ file: URI(@path).path,
109
+ line: node.location.start_line,
110
+ )
111
+ return if result.nil?
112
+
113
+ result[:routes].each do |route|
114
+ if route[:action].nil?
115
+ add_locations(@index[route[:controller]])
116
+ else
117
+ add_route_locations(route[:controller], route[:action])
118
+ end
119
+ end
120
+ end
121
+
122
+ # In a model, a column's attribute method (`title`, `self.title = ...`, `title_changed?`) -> its line in the
123
+ # schema file. Skipped when the method really exists (e.g. an overridden attribute), so we don't duplicate
124
+ # Ruby LSP.
125
+ #
126
+ # @param message [String]
127
+ #
128
+ # @return [void]
129
+ #
130
+ def handle_column(message)
131
+ return if @client.nil? || resolves_on_current_class?(message)
132
+
133
+ column = message[ATTRIBUTE_METHOD, :column]
134
+ return if column.nil?
135
+
136
+ result = @client.delegate_request(
137
+ server_addon_name: SERVER_ADDON_NAME,
138
+ request_name: 'column_location',
139
+ model: @nesting.join('::'),
140
+ column: column,
141
+ )
142
+ return if result.nil?
143
+
144
+ path, line = result[:location].split(/:(?=\d+\z)/)
145
+ @response_builder << Interface::Location.new(
146
+ uri: URI::Generic.from_path(path: path).to_s,
147
+ range: Interface::Range.new(
148
+ start: Interface::Position.new(line: line.to_i - 1, character: 0),
149
+ end: Interface::Position.new(line: line.to_i - 1, character: 0),
150
+ ),
151
+ )
152
+ end
153
+
154
+ # @return [Boolean]
155
+ def in_model?
156
+ @path.include?('/app/models/') && !@nesting.empty?
157
+ end
158
+
159
+ # @return [Boolean] `config/routes.rb` or a `draw`n file under `config/routes/`
160
+ def in_routes_file?
161
+ @path.match?(%r{/config/routes(?:\.rb\z|/.+\.rb\z)})
162
+ end
163
+
164
+ # Prefers the action method; falls back to the controller class (e.g. an implicit, view-only action)
165
+ #
166
+ # @param controller [String] e.g. `Api::V1::CompaniesController`
167
+ # @param action [String]
168
+ #
169
+ # @return [void]
170
+ #
171
+ def add_route_locations(controller, action)
172
+ entries = @index.resolve_method(action, controller)
173
+ entries = @index[controller] if entries.nil? || entries.empty?
174
+ add_locations(entries)
175
+ end
176
+
177
+ # Only from controllers/views. For a plain call in a controller, only when the method doesn't exist on the
178
+ # controller (so we don't duplicate what Ruby LSP already found)
179
+ #
180
+ # @param node [Prism::CallNode] `foo`, `self.foo`, `helpers.foo`, or `view_context.foo`
181
+ # @param message [String]
182
+ #
183
+ # @return [void]
184
+ #
185
+ def handle_helper(node, message)
186
+ return if !in_controller? && !in_view?
187
+ return if in_controller? && !proxy_receiver?(node) && resolves_on_current_class?(message)
188
+
189
+ entries = (@index[message] || []).select { |entry| helper_method?(entry) }
190
+ add_locations(sort_helpers(entries))
191
+ end
192
+
193
+ # @param node [Prism::CallNode]
194
+ #
195
+ # @return [Boolean] `helpers.foo` or `view_context.foo`
196
+ #
197
+ def proxy_receiver?(node)
198
+ receiver = node.receiver
199
+ receiver.is_a?(Prism::CallNode) && self_receiver?(receiver) && HELPER_PROXIES.include?(receiver.message)
200
+ end
201
+
202
+ # @return [Boolean]
203
+ def resolves_on_current_class?(message)
204
+ return false if @nesting.empty?
205
+
206
+ entries = @index.resolve_method(message, @nesting.join('::'))
207
+ !entries.nil? && !entries.empty?
208
+ end
209
+
210
+ # @return [Boolean]
211
+ def helper_method?(entry)
212
+ entry.is_a?(RubyIndexer::Entry::Member) && entry.uri.to_s.include?(HELPERS_DIR)
213
+ end
214
+
215
+ # The helper named after the current controller/view folder (e.g. `PostsHelper`) first, then alphabetical
216
+ #
217
+ # @param entries [Array<RubyIndexer::Entry::Member>]
218
+ #
219
+ # @return [Array<RubyIndexer::Entry::Member>]
220
+ #
221
+ def sort_helpers(entries)
222
+ matching = matching_helper_name
223
+
224
+ entries.sort_by { |entry| [(entry.owner&.name == matching) ? 0 : 1, entry.owner&.name.to_s] }
225
+ end
226
+
227
+ # @return [String, nil] e.g. `PostsHelper` for `PostsController` or `app/views/posts/index.html.erb`
228
+ def matching_helper_name
229
+ if in_controller?
230
+ @nesting.join('::').sub(/Controller\z/, 'Helper')
231
+ elsif in_view?
232
+ folder = @path[%r{/app/views/(.+)/[^/]+\z}, 1]
233
+ return if folder.nil?
234
+
235
+ "#{folder.split('/').map { |part| part.split('_').map(&:capitalize).join }.join('::')}Helper"
236
+ end
237
+ end
238
+
239
+ # @return [Boolean]
240
+ def in_controller?
241
+ @path.include?('/app/controllers/') && !@nesting.empty? && @nesting.last.end_with?('Controller')
242
+ end
243
+
244
+ # @return [Boolean]
245
+ def in_view?
246
+ @path.include?('/app/views/') && @path.end_with?('.erb')
247
+ end
248
+
249
+ # Skips locations already in the response -- Ruby LSP's own listener runs first and, for receivers it can't
250
+ # type (`helpers.foo`, ERB), already matches methods by name
251
+ #
252
+ # @param entries [Array<RubyIndexer::Entry>, nil]
253
+ #
254
+ # @return [void]
255
+ #
256
+ def add_locations(entries)
257
+ return if entries.nil?
258
+
259
+ entries.each do |entry|
260
+ uri = entry.uri.to_s
261
+ range = range_from_location(entry.location)
262
+ next if already_added?(uri, range.start.line)
263
+
264
+ @response_builder << Interface::Location.new(uri: uri, range: range)
265
+ end
266
+ end
267
+
268
+ # @param uri [String]
269
+ # @param line [Integer] 0-based
270
+ #
271
+ # @return [Boolean]
272
+ #
273
+ def already_added?(uri, line)
274
+ @response_builder.response.any? do |item|
275
+ item_uri, item_range =
276
+ if item.is_a?(Interface::LocationLink)
277
+ [item.target_uri, item.target_range]
278
+ else
279
+ [item.uri, item.range]
280
+ end
281
+
282
+ item_uri == uri && item_range.start.line == line
283
+ end
284
+ end
285
+ end
286
+ end
287
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLsp
4
+ module Extras
5
+ # Runs inside the Rails process booted by ruby-lsp-rails. Resolves routes to the controller class and action they
6
+ # route to (by route helper name, e.g. `companies_path`, or by where the route is declared in a routes file), and
7
+ # model columns to their line in the schema file.
8
+ class ServerAddon < ::RubyLsp::Rails::ServerAddon
9
+ # Must match `RubyLsp::Extras::SERVER_ADDON_NAME` in addon.rb
10
+ #
11
+ # @return [String]
12
+ #
13
+ def name
14
+ 'Extras'
15
+ end
16
+
17
+ # @param request [String]
18
+ # @param params [Hash]
19
+ #
20
+ # @return [void]
21
+ #
22
+ def execute(request, params)
23
+ case request
24
+ when 'controller_action'
25
+ with_request_error_handling(request) do
26
+ send_result(controller_action(params[:name]))
27
+ end
28
+ when 'routes_at'
29
+ with_request_error_handling(request) do
30
+ send_result(routes_at(params[:file], params[:line]))
31
+ end
32
+ when 'column_location'
33
+ with_request_error_handling(request) do
34
+ send_result(column_location(params[:model], params[:column]))
35
+ end
36
+ else
37
+ send_result(nil)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ # @param helper_name [String] e.g. `companies_path` or `edit_company_url`
44
+ #
45
+ # @return [Hash, nil]
46
+ #
47
+ def controller_action(helper_name)
48
+ match_data = helper_name.to_s.match(/\A(.+)_(?:path|url)\z/)
49
+ return if match_data.nil?
50
+
51
+ # In Rails 8, `named_routes` is not populated by default
52
+ if ::Rails.application.respond_to?(:reload_routes_unless_loaded)
53
+ ::Rails.application.reload_routes_unless_loaded
54
+ end
55
+
56
+ route = ::Rails.application.routes.named_routes.get(match_data[1])
57
+ controller = route&.defaults&.dig(:controller)
58
+ return if controller.blank?
59
+
60
+ { controller: "#{controller}_controller".camelize, action: route.defaults[:action].to_s }
61
+ end
62
+
63
+ # Routes declared on `line` of `file`, grouped by controller. `action` is only set when the line declares a
64
+ # single action (e.g. `get 'x', to: 'foo#bar'`); multi-action lines (e.g. `resources :posts`) leave it `nil`.
65
+ #
66
+ # Relies on `ActionDispatch::Routing::Mapper.route_source_locations` (on in development since Rails 7.1)
67
+ #
68
+ # @param file [String] absolute path, e.g. `/Easy/config/routes.rb`
69
+ # @param line [Integer] 1-based
70
+ #
71
+ # @return [Hash] `{ routes: [{ controller: 'PostsController', action: nil }] }`
72
+ #
73
+ def routes_at(file, line)
74
+ # `source_location` is absolute here, but may be relative to Rails.root depending on Rails version
75
+ location = "#{file}:#{line}"
76
+
77
+ routes = ::Rails.application.routes.routes.select do |route|
78
+ next false unless route.respond_to?(:source_location)
79
+ next false unless route.source_location
80
+ next false unless route.defaults[:controller]
81
+
82
+ ::Rails.root.join(route.source_location).to_s == location
83
+ end
84
+
85
+ grouped = routes.group_by { |route| route.defaults[:controller] }.map do |controller, controller_routes|
86
+ actions = controller_routes.map { |route| route.defaults[:action].to_s }
87
+ actions.uniq!
88
+
89
+ { controller: "#{controller}_controller".camelize, action: (actions.one? ? actions.first : nil) }
90
+ end
91
+
92
+ { routes: grouped }
93
+ end
94
+
95
+ # Where `column` of `model`'s table is declared in the schema file (`db/schema.rb`). The primary key (and any
96
+ # column without its own `t.<type>` line) points at the `create_table` line.
97
+ #
98
+ # @param model [String] e.g. `Company`
99
+ # @param column [String] e.g. `name`
100
+ #
101
+ # @return [Hash, nil] `{ location: '/Easy/db/schema.rb:123' }`
102
+ #
103
+ def column_location(model, column)
104
+ const = ActiveSupport::Inflector.safe_constantize(model)
105
+ return unless const.is_a?(Class)
106
+ return unless const < ActiveRecord::Base
107
+
108
+ return if const.abstract_class?
109
+
110
+ return unless const.table_exists?
111
+ return unless const.column_names.include?(column)
112
+
113
+ db_config = const.connection.pool.db_config
114
+ schema_file =
115
+ if ActiveRecord::Tasks::DatabaseTasks.respond_to?(:schema_dump_path)
116
+ ActiveRecord::Tasks::DatabaseTasks.schema_dump_path(db_config)
117
+ else
118
+ # Rails 6.1
119
+ ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.name)
120
+ end
121
+ return if schema_file.nil?
122
+
123
+ return unless File.exist?(schema_file)
124
+
125
+ line = schema_line(File.readlines(schema_file), const.table_name, column)
126
+ return if line.nil?
127
+
128
+ { location: "#{schema_file}:#{line}" }
129
+ end
130
+
131
+ # @param lines [Array<String>] schema.rb lines
132
+ # @param table [String]
133
+ # @param column [String]
134
+ #
135
+ # @return [Integer, nil] 1-based line
136
+ #
137
+ def schema_line(lines, table, column)
138
+ table_index = lines.index { |l| l.match?(/^\s*create_table\s+"#{Regexp.escape(table)}"/) }
139
+ return if table_index.nil?
140
+
141
+ column_pattern = /^\s*t\.\w+\s+"#{Regexp.escape(column)}"/
142
+ lines[(table_index + 1)..].each_with_index do |l, offset|
143
+ break if l.match?(/^\s*end\s*$/)
144
+
145
+ return table_index + offset + 2 if l.match?(column_pattern)
146
+ end
147
+
148
+ table_index + 1
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLsp
4
+ module Extras
5
+ module VERSION
6
+ MAJOR = 0
7
+ MINOR = 2
8
+ PATCH = 0
9
+ TINY = PATCH
10
+
11
+ # Set PRE to nil unless it's a pre-release (beta, rc, etc.)
12
+ PRE = nil
13
+
14
+ STRING = [MAJOR, MINOR, PATCH, PRE].compact.join('.').freeze
15
+
16
+ # :nodoc:
17
+ def self.to_s
18
+ STRING
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'completion'
4
+ require_relative 'definition'
5
+ require_relative 'hover'
6
+ require_relative 'partial_index'
7
+ require_relative 'partial_resolver'
8
+ require_relative 'util'
9
+ require_relative 'version'
10
+
11
+ module RubyLsp
12
+ module RailsPartial
13
+ # Ruby LSP add-on that provides go-to-definition and completion for the partial name string of
14
+ # a render call.
15
+ #
16
+ # The view root is fixed to app/views (this add-on does not support prepend/append_view_path or
17
+ # independent engines).
18
+ class Addon < ::RubyLsp::Addon
19
+ WATCHER_REGISTRATION_ID = 'rails-partial-watcher'
20
+
21
+ # :nodoc:
22
+ def activate(global_state, outgoing_queue)
23
+ views_path = File.join(global_state.workspace_path, 'app', 'views')
24
+ @index = PartialIndex.new(views_path)
25
+ @resolver = PartialResolver.new(@index, views_path)
26
+
27
+ register_file_watcher(global_state, outgoing_queue)
28
+ end
29
+
30
+ # :nodoc:
31
+ def deactivate
32
+ end
33
+
34
+ # :nodoc:
35
+ def name
36
+ 'Rails Partial'
37
+ end
38
+
39
+ # :nodoc:
40
+ def version
41
+ VERSION
42
+ end
43
+
44
+ # :nodoc:
45
+ def create_definition_listener(response_builder, uri, node_context, dispatcher)
46
+ return unless @resolver
47
+
48
+ Definition.new(response_builder, uri, node_context, @resolver, dispatcher)
49
+ end
50
+
51
+ # :nodoc:
52
+ def create_completion_listener(response_builder, node_context, dispatcher, uri)
53
+ return unless @resolver
54
+
55
+ Completion.new(response_builder, node_context, @resolver, uri, dispatcher)
56
+ end
57
+
58
+ # :nodoc:
59
+ def create_hover_listener(response_builder, node_context, dispatcher)
60
+ return unless @resolver
61
+
62
+ Hover.new(response_builder, node_context, @resolver, dispatcher)
63
+ end
64
+
65
+ # Watches partial changes under app/views and updates the index incrementally.
66
+ def workspace_did_change_watched_files(changes)
67
+ return unless @index
68
+
69
+ changes.each do |change|
70
+ path = Util.uri_to_path(change[:uri])
71
+ next unless path
72
+
73
+ case change[:type]
74
+ when Constant::FileChangeType::CREATED, Constant::FileChangeType::CHANGED
75
+ @index.add(path)
76
+ when Constant::FileChangeType::DELETED
77
+ @index.remove(path)
78
+ end
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ # :nodoc:
85
+ def register_file_watcher(global_state, outgoing_queue)
86
+ return unless global_state.supports_watching_files
87
+
88
+ outgoing_queue << Request.register_watched_files(
89
+ 1,
90
+ Interface::RelativePattern.new(
91
+ base_uri: global_state.workspace_uri.to_s,
92
+ pattern: 'app/views/**/_*',
93
+ ),
94
+ registration_id: WATCHER_REGISTRATION_ID,
95
+ )
96
+ end
97
+ end
98
+ end
99
+ end