Skip to content

Commit d63f245

Browse files
authored
Display index and foreign key details when hovering over a model (#588)
* display index and foreign key details when hovering over a model * remove italics from heading * cache index support to avoid repeated rescue
1 parent c66472c commit d63f245

File tree

4 files changed

+108
-32
lines changed

4 files changed

+108
-32
lines changed

lib/ruby_lsp/ruby_lsp_rails/hover.rb

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,17 +57,38 @@ def generate_column_content(name)
5757
category: :documentation,
5858
) if schema_file
5959

60-
@response_builder.push(
61-
model[:columns].map do |name, type, default_value, nullable|
62-
primary_key_suffix = " (PK)" if model[:primary_keys].include?(name)
63-
suffixes = []
64-
suffixes << "default: #{format_default(default_value, type)}" if default_value
65-
suffixes << "not null" unless nullable || primary_key_suffix
66-
suffix_string = " - #{suffixes.join(" - ")}" if suffixes.any?
67-
"**#{name}**: #{type}#{primary_key_suffix}#{suffix_string}\n"
68-
end.join("\n"),
69-
category: :documentation,
70-
)
60+
if model[:columns].any?
61+
@response_builder.push(
62+
"### Columns",
63+
category: :documentation,
64+
)
65+
@response_builder.push(
66+
model[:columns].map do |name, type, default_value, nullable|
67+
primary_key_suffix = " (PK)" if model[:primary_keys].include?(name)
68+
foreign_key_suffix = " (FK)" if model[:foreign_keys].include?(name)
69+
suffixes = []
70+
suffixes << "default: #{format_default(default_value, type)}" if default_value
71+
suffixes << "not null" unless nullable || primary_key_suffix
72+
suffix_string = " - #{suffixes.join(" - ")}" if suffixes.any?
73+
"- **#{name}**: #{type}#{primary_key_suffix}#{foreign_key_suffix}#{suffix_string}\n"
74+
end.join("\n"),
75+
category: :documentation,
76+
)
77+
end
78+
79+
if model[:indexes].any?
80+
@response_builder.push(
81+
"### Indexes",
82+
category: :documentation,
83+
)
84+
@response_builder.push(
85+
model[:indexes].map do |index|
86+
uniqueness = index[:unique] ? " (unique)" : ""
87+
"- **#{index[:name]}** (#{index[:columns].join(",")})#{uniqueness}"
88+
end.join("\n"),
89+
category: :documentation,
90+
)
91+
end
7192
end
7293

7394
#: (String default_value, String type) -> String

lib/ruby_lsp/ruby_lsp_rails/server.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,8 @@ def resolve_database_info_from_model(model_name)
364364
info = {
365365
columns: const.columns.map { |column| [column.name, column.type, column.default, column.null] },
366366
primary_keys: Array(const.primary_key),
367+
foreign_keys: collect_model_foreign_keys(const),
368+
indexes: collect_model_indexes(const),
367369
}
368370

369371
if ActiveRecord::Tasks::DatabaseTasks.respond_to?(:schema_dump_path)
@@ -435,6 +437,36 @@ def clear_file_system_resolver_hooks
435437
::ActionView::PathRegistry.file_system_resolver_hooks.clear
436438
end
437439
end
440+
441+
def collect_model_foreign_keys(model)
442+
return [] unless model.connection.respond_to?(:supports_foreign_keys?) &&
443+
model.connection.supports_foreign_keys?
444+
445+
model.connection.foreign_keys(model.table_name).map do |key_definition|
446+
key_definition.options[:column]
447+
end
448+
end
449+
450+
def collect_model_indexes(model)
451+
return [] unless database_supports_indexing?(model)
452+
453+
model.connection.indexes(model.table_name).map do |index_definition|
454+
{
455+
name: index_definition.name,
456+
columns: index_definition.columns,
457+
unique: index_definition.unique,
458+
}
459+
end
460+
end
461+
462+
def database_supports_indexing?(model)
463+
return @database_supports_indexing if instance_variable_defined?(:@database_supports_indexing)
464+
465+
model.connection.indexes(model.table_name)
466+
@database_supports_indexing = true
467+
rescue NotImplementedError
468+
@database_supports_indexing = false
469+
end
438470
end
439471
end
440472
end

test/ruby_lsp_rails/hover_test.rb

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ class HoverTest < ActiveSupport::TestCase
2020
["active", "boolean", "true", false],
2121
],
2222
primary_keys: ["id"],
23+
foreign_keys: ["country_id"],
24+
indexes: [{ name: "index_users_on_country_id", columns: ["country_id"], unique: false }],
2325
}
2426

2527
RunnerClient.any_instance.stubs(model: expected_response)
@@ -41,21 +43,25 @@ class User < ApplicationRecord
4143
4244
[Schema](#{URI::Generic.from_path(path: dummy_root + "/db/schema.rb")})
4345
44-
**id**: integer (PK)
46+
### Columns
47+
- **id**: integer (PK)
4548
46-
**first_name**: string - default: ""
49+
- **first_name**: string - default: ""
4750
48-
**last_name**: string
51+
- **last_name**: string
4952
50-
**age**: integer - default: 0
53+
- **age**: integer - default: 0
5154
52-
**created_at**: datetime - not null
55+
- **created_at**: datetime - not null
5356
54-
**updated_at**: datetime - not null
57+
- **updated_at**: datetime - not null
5558
56-
**country_id**: integer - not null
59+
- **country_id**: integer (FK) - not null
5760
58-
**active**: boolean - default: true - not null
61+
- **active**: boolean - default: true - not null
62+
63+
### Indexes
64+
- **index_users_on_country_id** (country_id)
5965
CONTENT
6066
end
6167

@@ -73,6 +79,8 @@ class User < ApplicationRecord
7379
["active", "boolean", "true", false],
7480
],
7581
primary_keys: ["id"],
82+
foreign_keys: [],
83+
indexes: [{ name: "index_users_on_country_id", columns: ["country_id"], unique: true }],
7684
}
7785

7886
RunnerClient.any_instance.stubs(model: expected_response)
@@ -96,21 +104,25 @@ class User < ApplicationRecord
96104
97105
[Schema](#{URI::Generic.from_path(path: dummy_root + "/db/schema.rb")})
98106
99-
**id**: integer (PK)
107+
### Columns
108+
- **id**: integer (PK)
109+
110+
- **first_name**: string - default: ""
100111
101-
**first_name**: string - default: ""
112+
- **last_name**: string
102113
103-
**last_name**: string
114+
- **age**: integer - default: 0
104115
105-
**age**: integer - default: 0
116+
- **created_at**: datetime - not null
106117
107-
**created_at**: datetime - not null
118+
- **updated_at**: datetime - not null
108119
109-
**updated_at**: datetime - not null
120+
- **country_id**: integer - not null
110121
111-
**country_id**: integer - not null
122+
- **active**: boolean - default: true - not null
112123
113-
**active**: boolean - default: true - not null
124+
### Indexes
125+
- **index_users_on_country_id** (country_id) (unique)
114126
CONTENT
115127
end
116128

@@ -125,6 +137,8 @@ class User < ApplicationRecord
125137
["updated_at", "datetime"],
126138
],
127139
primary_keys: ["order_id", "product_id"],
140+
foreign_keys: [],
141+
indexes: [],
128142
}
129143

130144
RunnerClient.any_instance.stubs(model: expected_response)
@@ -146,15 +160,16 @@ class CompositePrimaryKey < ApplicationRecord
146160
147161
[Schema](#{URI::Generic.from_path(path: dummy_root + "/db/schema.rb")})
148162
149-
**order_id**: integer (PK)
163+
### Columns
164+
- **order_id**: integer (PK)
150165
151-
**product_id**: integer (PK)
166+
- **product_id**: integer (PK)
152167
153-
**note**: string - not null
168+
- **note**: string - not null
154169
155-
**created_at**: datetime - not null
170+
- **created_at**: datetime - not null
156171
157-
**updated_at**: datetime - not null
172+
- **updated_at**: datetime - not null
158173
CONTENT
159174
end
160175

@@ -163,6 +178,8 @@ class CompositePrimaryKey < ApplicationRecord
163178
schema_file: "#{dummy_root}/db/structure.sql",
164179
columns: [],
165180
primary_keys: [],
181+
foreign_keys: [],
182+
indexes: [],
166183
}
167184

168185
RunnerClient.any_instance.stubs(model: expected_response)
@@ -185,6 +202,8 @@ class User < ApplicationRecord
185202
schema_file: nil,
186203
columns: [],
187204
primary_keys: [],
205+
foreign_keys: [],
206+
indexes: [],
188207
}
189208

190209
RunnerClient.any_instance.stubs(model: expected_response)

test/ruby_lsp_rails/runner_client_test.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,12 @@ class RunnerClientTest < ActiveSupport::TestCase
5757
["active", "boolean", true, false],
5858
]
5959
end
60+
foreign_keys = ["country_id"]
61+
indexes = [{ name: "index_users_on_country_id", columns: ["country_id"], unique: false }]
6062
response = T.must(@client.model("User"))
6163
assert_equal(columns, response.fetch(:columns))
64+
assert_equal(foreign_keys, response.fetch(:foreign_keys))
65+
assert_equal(indexes, response.fetch(:indexes))
6266
assert_match(%r{db/schema\.rb$}, response.fetch(:schema_file))
6367
end
6468

0 commit comments

Comments
 (0)