-
Notifications
You must be signed in to change notification settings - Fork 21
/
server.rb
183 lines (154 loc) · 5.58 KB
/
server.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# typed: false
# frozen_string_literal: true
require "json"
# NOTE: We should avoid printing to stderr since it causes problems. We never read the standard error pipe from the
# client, so it will become full and eventually hang or crash. Instead, return a response with an `error` key.
module RubyLsp
module Rails
class Server
VOID = Object.new
def initialize
$stdin.sync = true
$stdout.sync = true
$stdin.binmode
$stdout.binmode
@running = true
end
def start
# Load routes if they haven't been loaded yet (see https://github.com/rails/rails/pull/51614).
routes_reloader = ::Rails.application.routes_reloader
routes_reloader.execute_unless_loaded if routes_reloader&.respond_to?(:execute_unless_loaded)
initialize_result = { result: { message: "ok" } }.to_json
$stdout.write("Content-Length: #{initialize_result.length}\r\n\r\n#{initialize_result}")
while @running
headers = $stdin.gets("\r\n\r\n")
json = $stdin.read(headers[/Content-Length: (\d+)/i, 1].to_i)
request = JSON.parse(json, symbolize_names: true)
response = execute(request.fetch(:method), request[:params])
next if response == VOID
json_response = response.to_json
$stdout.write("Content-Length: #{json_response.length}\r\n\r\n#{json_response}")
end
end
def execute(request, params)
case request
when "shutdown"
@running = false
VOID
when "model"
resolve_database_info_from_model(params.fetch(:name))
when "association_target_location"
resolve_association_target(params)
when "reload"
::Rails.application.reloader.reload!
VOID
when "route_location"
route_location(params.fetch(:name))
when "route_info"
resolve_route_info(params)
else
VOID
end
rescue => e
{ error: e.full_message(highlight: false) }
end
private
def resolve_route_info(requirements)
if requirements[:controller]
requirements[:controller] = requirements.fetch(:controller).underscore.delete_suffix("_controller")
end
# In Rails 7.2 we can use `from_requirements, otherwise we fall back to a private API
route = if ::Rails.application.routes.respond_to?(:from_requirements)
::Rails.application.routes.from_requirements(requirements)
else
::Rails.application.routes.routes.find { |route| route.requirements == requirements }
end
if route&.source_location
file, _, line = route.source_location.rpartition(":")
body = {
source_location: [::Rails.root.join(file).to_s, line],
verb: route.verb,
path: route.path.spec.to_s,
}
{ result: body }
else
{ result: nil }
end
end
# Older versions of Rails don't support `route_source_locations`.
# We also check that it's enabled.
if ActionDispatch::Routing::Mapper.respond_to?(:route_source_locations) &&
ActionDispatch::Routing::Mapper.route_source_locations
def route_location(name)
match_data = name.match(/^(.+)(_path|_url)$/)
return { result: nil } unless match_data
key = match_data[1]
# A token could match the _path or _url pattern, but not be an actual route.
route = ::Rails.application.routes.named_routes.get(key)
return { result: nil } unless route&.source_location
{
result: {
location: ::Rails.root.join(route.source_location).to_s,
},
}
rescue => e
{ error: e.full_message(highlight: false) }
end
else
def route_location(name)
{ result: nil }
end
end
def resolve_database_info_from_model(model_name)
const = ActiveSupport::Inflector.safe_constantize(model_name)
unless active_record_model?(const)
return {
result: nil,
}
end
info = {
result: {
columns: const.columns.map { |column| [column.name, column.type] },
primary_keys: Array(const.primary_key),
},
}
if ActiveRecord::Tasks::DatabaseTasks.respond_to?(:schema_dump_path)
info[:result][:schema_file] =
ActiveRecord::Tasks::DatabaseTasks.schema_dump_path(const.connection.pool.db_config)
end
info
rescue => e
{ error: e.full_message(highlight: false) }
end
def resolve_association_target(params)
const = ActiveSupport::Inflector.safe_constantize(params[:model_name])
unless active_record_model?(const)
return {
result: nil,
}
end
association_klass = const.reflect_on_association(params[:association_name].intern).klass
source_location = Object.const_source_location(association_klass.to_s)
{
result: {
location: source_location.first + ":" + source_location.second.to_s,
},
}
rescue NameError
{
result: nil,
}
end
def active_record_model?(const)
!!(
const &&
defined?(ActiveRecord) &&
const.is_a?(Class) &&
ActiveRecord::Base > const && # We do this 'backwards' in case the class overwrites `<`
!const.abstract_class?
)
end
end
end
end
RubyLsp::Rails::Server.new.start if ARGV.first == "start"