mbeditor 0.5.6 → 0.6.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 +4 -4
- data/CHANGELOG.md +20 -0
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +53 -28
- data/app/assets/javascripts/mbeditor/components/FileTree.js +168 -116
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +361 -84
- data/app/assets/javascripts/mbeditor/editor_plugins.js +32 -4
- data/app/assets/javascripts/mbeditor/file_service.js +7 -2
- data/app/assets/javascripts/mbeditor/git_service.js +2 -1
- data/app/assets/javascripts/mbeditor/tab_manager.js +1 -1
- data/app/assets/stylesheets/mbeditor/editor.css +97 -65
- data/app/controllers/mbeditor/editors_controller.rb +21 -8
- data/app/services/mbeditor/rails_related_files_service.rb +234 -0
- data/config/routes.rb +1 -0
- data/lib/mbeditor/configuration.rb +2 -1
- data/lib/mbeditor/version.rb +1 -1
- metadata +3 -2
|
@@ -7,7 +7,8 @@ var GitService = (function () {
|
|
|
7
7
|
gitInfo: data,
|
|
8
8
|
gitInfoError: null
|
|
9
9
|
};
|
|
10
|
-
|
|
10
|
+
var gitSig = function(arr) { return arr.map(function(f) { return f.path + '\x00' + f.status; }).join('\x01'); };
|
|
11
|
+
if (gitSig(files) !== gitSig(current)) {
|
|
11
12
|
stateUpdate.gitFiles = files;
|
|
12
13
|
}
|
|
13
14
|
EditorStore.setState(stateUpdate);
|
|
@@ -58,33 +58,30 @@ html, body, #mbeditor-root {
|
|
|
58
58
|
|
|
59
59
|
/* ── Sidebar ─────────────────────────────────────────────── */
|
|
60
60
|
.ide-sidebar {
|
|
61
|
-
width: 280px;
|
|
62
61
|
min-width: 280px;
|
|
63
62
|
max-width: 560px;
|
|
64
63
|
background: var(--ide-panel-bg);
|
|
65
|
-
border-right: 1px solid var(--ide-border);
|
|
66
64
|
display: flex;
|
|
67
65
|
flex-direction: column;
|
|
68
66
|
overflow: hidden;
|
|
69
67
|
flex-shrink: 0;
|
|
70
|
-
transition: width 0.15s ease;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
.ide-sidebar.ide-sidebar-collapsed {
|
|
74
|
-
min-width: 0;
|
|
75
|
-
max-width: none;
|
|
76
68
|
}
|
|
77
69
|
|
|
78
|
-
/* ──
|
|
79
|
-
.
|
|
70
|
+
/* ── Activity Bar (VSCode-style, always visible) ───────── */
|
|
71
|
+
.ide-activity-bar {
|
|
72
|
+
width: 48px;
|
|
73
|
+
min-width: 48px;
|
|
74
|
+
flex-shrink: 0;
|
|
75
|
+
background: var(--ide-panel-bg);
|
|
76
|
+
border-right: 1px solid var(--ide-border);
|
|
80
77
|
display: flex;
|
|
81
78
|
flex-direction: column;
|
|
82
79
|
align-items: center;
|
|
83
|
-
height: 100%;
|
|
84
80
|
padding: 4px 0;
|
|
81
|
+
z-index: 10;
|
|
85
82
|
}
|
|
86
83
|
|
|
87
|
-
.
|
|
84
|
+
.ide-activity-bar-top {
|
|
88
85
|
display: flex;
|
|
89
86
|
flex-direction: column;
|
|
90
87
|
align-items: center;
|
|
@@ -92,7 +89,7 @@ html, body, #mbeditor-root {
|
|
|
92
89
|
flex: 1;
|
|
93
90
|
}
|
|
94
91
|
|
|
95
|
-
.
|
|
92
|
+
.ide-activity-bar-bottom {
|
|
96
93
|
display: flex;
|
|
97
94
|
flex-direction: column;
|
|
98
95
|
align-items: center;
|
|
@@ -100,7 +97,7 @@ html, body, #mbeditor-root {
|
|
|
100
97
|
padding-bottom: 4px;
|
|
101
98
|
}
|
|
102
99
|
|
|
103
|
-
.
|
|
100
|
+
.ide-activity-btn {
|
|
104
101
|
width: 36px;
|
|
105
102
|
height: 36px;
|
|
106
103
|
background: transparent;
|
|
@@ -115,25 +112,28 @@ html, body, #mbeditor-root {
|
|
|
115
112
|
transition: background 0.12s ease, color 0.12s ease;
|
|
116
113
|
}
|
|
117
114
|
|
|
118
|
-
.
|
|
115
|
+
.ide-activity-btn:hover {
|
|
119
116
|
background: var(--ide-hover-bg);
|
|
120
117
|
color: var(--ide-text);
|
|
121
118
|
}
|
|
122
119
|
|
|
123
|
-
.
|
|
120
|
+
.ide-activity-btn.active {
|
|
124
121
|
color: var(--ide-accent-fg);
|
|
125
122
|
border-left: 2px solid var(--ide-accent-fg);
|
|
126
123
|
}
|
|
127
124
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
125
|
+
.sidebar-panel-title {
|
|
126
|
+
font-size: 11px;
|
|
127
|
+
font-weight: 700;
|
|
128
|
+
text-transform: uppercase;
|
|
129
|
+
letter-spacing: 0.08em;
|
|
130
|
+
color: var(--ide-fg-muted, var(--ide-text-muted, #888));
|
|
131
|
+
padding: 10px 12px 6px;
|
|
132
|
+
flex-shrink: 0;
|
|
133
|
+
border-bottom: 1px solid var(--ide-border);
|
|
135
134
|
}
|
|
136
135
|
|
|
136
|
+
|
|
137
137
|
.panel-divider {
|
|
138
138
|
flex-shrink: 0;
|
|
139
139
|
cursor: col-resize;
|
|
@@ -146,13 +146,6 @@ html, body, #mbeditor-root {
|
|
|
146
146
|
border-right: 1px solid var(--ide-border);
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
.ide-sidebar-collapsed + .sidebar-divider {
|
|
150
|
-
cursor: default;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
.ide-sidebar-collapsed + .sidebar-divider:hover {
|
|
154
|
-
background: var(--ide-panel-bg-alt);
|
|
155
|
-
}
|
|
156
149
|
|
|
157
150
|
.pane-divider {
|
|
158
151
|
width: 4px;
|
|
@@ -179,33 +172,6 @@ html, body, #mbeditor-root {
|
|
|
179
172
|
gap: 6px;
|
|
180
173
|
}
|
|
181
174
|
|
|
182
|
-
.ide-sidebar-tabs {
|
|
183
|
-
display: flex;
|
|
184
|
-
border-bottom: 1px solid var(--ide-border);
|
|
185
|
-
flex-shrink: 0;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
.ide-sidebar-tab {
|
|
189
|
-
flex: 1;
|
|
190
|
-
padding: 5px 0;
|
|
191
|
-
font-size: 11px;
|
|
192
|
-
text-align: center;
|
|
193
|
-
cursor: pointer;
|
|
194
|
-
color: var(--ide-text-muted);
|
|
195
|
-
border: none;
|
|
196
|
-
border-radius: 0;
|
|
197
|
-
background: transparent;
|
|
198
|
-
transition: color 0.15s, background 0.15s;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
.ide-sidebar-tab.ide-sidebar-tab-icon {
|
|
202
|
-
flex: 0 0 auto;
|
|
203
|
-
padding: 5px 10px;
|
|
204
|
-
font-size: 14px;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
.ide-sidebar-tab:hover { color: var(--ide-text); background: var(--ide-input-bg); }
|
|
208
|
-
.ide-sidebar-tab.active { color: var(--ide-accent-fg); border-bottom: 2px solid var(--ide-accent-fg); }
|
|
209
175
|
|
|
210
176
|
.ide-sidebar-content {
|
|
211
177
|
flex: 1;
|
|
@@ -1069,6 +1035,76 @@ html, body, #mbeditor-root {
|
|
|
1069
1035
|
gap: 6px;
|
|
1070
1036
|
}
|
|
1071
1037
|
|
|
1038
|
+
/* ── Rails Panel ─────────────────────────────────────────── */
|
|
1039
|
+
.rails-panel {
|
|
1040
|
+
display: flex;
|
|
1041
|
+
flex-direction: column;
|
|
1042
|
+
overflow-y: auto;
|
|
1043
|
+
flex: 1;
|
|
1044
|
+
padding: 8px 0;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
.rails-panel-loading,
|
|
1048
|
+
.rails-panel-empty {
|
|
1049
|
+
padding: 12px 16px;
|
|
1050
|
+
color: var(--ide-text-muted);
|
|
1051
|
+
font-size: 12px;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
.rails-group {
|
|
1055
|
+
margin-bottom: 4px;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
.rails-group-title {
|
|
1059
|
+
padding: 4px 12px 2px;
|
|
1060
|
+
font-size: 10px;
|
|
1061
|
+
font-weight: 700;
|
|
1062
|
+
letter-spacing: 0.08em;
|
|
1063
|
+
text-transform: uppercase;
|
|
1064
|
+
color: var(--ide-text-muted);
|
|
1065
|
+
user-select: none;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
.rails-group-item {
|
|
1069
|
+
display: flex;
|
|
1070
|
+
align-items: center;
|
|
1071
|
+
gap: 6px;
|
|
1072
|
+
padding: 2px 12px 2px 16px;
|
|
1073
|
+
cursor: pointer;
|
|
1074
|
+
font-size: 12px;
|
|
1075
|
+
color: var(--ide-text);
|
|
1076
|
+
white-space: nowrap;
|
|
1077
|
+
overflow: hidden;
|
|
1078
|
+
text-overflow: ellipsis;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
.rails-group-item:hover {
|
|
1082
|
+
background: var(--ide-hover-bg);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
.rails-group-item-name {
|
|
1086
|
+
overflow: hidden;
|
|
1087
|
+
text-overflow: ellipsis;
|
|
1088
|
+
white-space: nowrap;
|
|
1089
|
+
flex: 1;
|
|
1090
|
+
min-width: 0;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
.rails-group-item-dirty {
|
|
1094
|
+
flex-shrink: 0;
|
|
1095
|
+
font-size: 8px;
|
|
1096
|
+
color: var(--ide-accent-fg);
|
|
1097
|
+
opacity: 0.85;
|
|
1098
|
+
line-height: 1;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
.rails-panel-overflow {
|
|
1102
|
+
padding: 4px 12px 8px;
|
|
1103
|
+
font-size: 11px;
|
|
1104
|
+
color: var(--ide-text-muted, #888);
|
|
1105
|
+
font-style: italic;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1072
1108
|
/* ── Git Status Panel ─────────────────────────────────────── */
|
|
1073
1109
|
.git-panel { padding: 8px; }
|
|
1074
1110
|
|
|
@@ -2109,14 +2145,6 @@ textarea {
|
|
|
2109
2145
|
align-items: center;
|
|
2110
2146
|
justify-content: center;
|
|
2111
2147
|
}
|
|
2112
|
-
.sidebar-strip-btn {
|
|
2113
|
-
padding: 0;
|
|
2114
|
-
margin: 0;
|
|
2115
|
-
border: none;
|
|
2116
|
-
display: inline-flex;
|
|
2117
|
-
align-items: center;
|
|
2118
|
-
justify-content: center;
|
|
2119
|
-
}
|
|
2120
2148
|
|
|
2121
2149
|
/* Context menu items */
|
|
2122
2150
|
.context-menu-item {
|
|
@@ -2272,3 +2300,7 @@ button:not(.pico-btn) { margin-bottom: 0; }
|
|
|
2272
2300
|
.project-actions[role="group"] button {
|
|
2273
2301
|
--pico-form-element-spacing-horizontal: 0;
|
|
2274
2302
|
}
|
|
2303
|
+
|
|
2304
|
+
.mbeditor-format-changed {
|
|
2305
|
+
background-color: rgba(80, 200, 100, 0.15);
|
|
2306
|
+
}
|
|
@@ -896,6 +896,16 @@ module Mbeditor
|
|
|
896
896
|
render json: { error: e.message, ok: false }, status: :unprocessable_content
|
|
897
897
|
end
|
|
898
898
|
|
|
899
|
+
# GET /mbeditor/related_files?path=...
|
|
900
|
+
def related_files
|
|
901
|
+
path = resolve_path(params[:path])
|
|
902
|
+
return render json: {}, status: :bad_request unless path
|
|
903
|
+
rel = relative_path(path)
|
|
904
|
+
custom = Array(Mbeditor.configuration.related_files_custom_paths)
|
|
905
|
+
result = RailsRelatedFilesService.find(workspace_root.to_s, rel, custom_paths: custom)
|
|
906
|
+
render json: result
|
|
907
|
+
end
|
|
908
|
+
|
|
899
909
|
# POST /mbeditor/format — rubocop -A on buffer content; returns corrected content WITHOUT saving to disk
|
|
900
910
|
#
|
|
901
911
|
# Accepts the current buffer content as `code` and formats it using a
|
|
@@ -1090,22 +1100,25 @@ module Mbeditor
|
|
|
1090
1100
|
0
|
|
1091
1101
|
end
|
|
1092
1102
|
|
|
1093
|
-
def build_tree(dir, max_depth: 10, depth: 0)
|
|
1103
|
+
def build_tree(dir, max_depth: 10, depth: 0, _excl: excluded_paths)
|
|
1094
1104
|
return [] if depth >= max_depth
|
|
1095
1105
|
|
|
1096
1106
|
entries = Dir.entries(dir).sort.reject { |entry| entry == "." || entry == ".." }
|
|
1097
1107
|
entries.filter_map do |name|
|
|
1098
1108
|
full = File.join(dir, name)
|
|
1099
1109
|
rel = relative_path(full)
|
|
1110
|
+
is_excl = excluded_path?(rel, name, _excl)
|
|
1100
1111
|
|
|
1101
1112
|
if File.directory?(full)
|
|
1102
|
-
|
|
1103
|
-
|
|
1113
|
+
# Skip recursing into excluded directories — avoids traversing node_modules etc.
|
|
1114
|
+
children = is_excl ? [] : build_tree(full, max_depth: max_depth, depth: depth + 1, _excl: _excl)
|
|
1115
|
+
node = { name: name, type: "folder", path: rel, children: children }
|
|
1116
|
+
node[:excluded] = true if is_excl
|
|
1104
1117
|
node
|
|
1105
1118
|
else
|
|
1106
|
-
|
|
1107
|
-
node =
|
|
1108
|
-
node[:excluded] = true if
|
|
1119
|
+
node = { name: name, type: "file", path: rel }
|
|
1120
|
+
node[:size] = (File.size(full) rescue nil) unless is_excl
|
|
1121
|
+
node[:excluded] = true if is_excl
|
|
1109
1122
|
node
|
|
1110
1123
|
end
|
|
1111
1124
|
end
|
|
@@ -1125,8 +1138,8 @@ module Mbeditor
|
|
|
1125
1138
|
Array(Mbeditor.configuration.ruby_def_include_dirs).map(&:to_s).reject(&:blank?)
|
|
1126
1139
|
end
|
|
1127
1140
|
|
|
1128
|
-
def excluded_path?(relative_path, name)
|
|
1129
|
-
|
|
1141
|
+
def excluded_path?(relative_path, name, excl = excluded_paths)
|
|
1142
|
+
excl.any? do |pattern|
|
|
1130
1143
|
if pattern.include?("/")
|
|
1131
1144
|
relative_path == pattern || relative_path.start_with?("#{pattern}/")
|
|
1132
1145
|
else
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mbeditor
|
|
4
|
+
# Given a workspace-relative file path, finds related Rails files grouped by
|
|
5
|
+
# type: controller, model, views, helper, tests, and custom directories.
|
|
6
|
+
#
|
|
7
|
+
# Only groups with at least one existing file are included in the result.
|
|
8
|
+
# All returned paths are workspace-relative strings.
|
|
9
|
+
module RailsRelatedFilesService
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# Returns a hash of groups. Example:
|
|
13
|
+
#
|
|
14
|
+
# {
|
|
15
|
+
# controller: [{path: "app/controllers/users_controller.rb", name: "users_controller.rb"}],
|
|
16
|
+
# model: [{path: "app/models/user.rb", name: "user.rb"}],
|
|
17
|
+
# views: [{path: "app/views/users/index.html.erb", name: "index.html.erb"}],
|
|
18
|
+
# helper: [{path: "app/helpers/users_helper.rb", name: "users_helper.rb"}],
|
|
19
|
+
# tests: [{path: "test/controllers/users_controller_test.rb", name: "users_controller_test.rb"}],
|
|
20
|
+
# custom: {"app/assets/javascripts/app" => [{path: "...", name: "..."}]}
|
|
21
|
+
# }
|
|
22
|
+
#
|
|
23
|
+
# Returns {} when the path does not match any known Rails convention.
|
|
24
|
+
def find(workspace_root, relative_path, custom_paths: [])
|
|
25
|
+
plural, singular = extract_resource_names(relative_path)
|
|
26
|
+
return {} unless plural && singular
|
|
27
|
+
|
|
28
|
+
result = {}
|
|
29
|
+
|
|
30
|
+
# ── controller ────────────────────────────────────────────────────────────
|
|
31
|
+
controller_path = "app/controllers/#{plural}_controller.rb"
|
|
32
|
+
if file_exists?(workspace_root, controller_path)
|
|
33
|
+
result[:controller] = [entry(controller_path)]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# ── model ─────────────────────────────────────────────────────────────────
|
|
37
|
+
model_path = "app/models/#{singular}.rb"
|
|
38
|
+
if file_exists?(workspace_root, model_path)
|
|
39
|
+
result[:model] = [entry(model_path)]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# ── views ─────────────────────────────────────────────────────────────────
|
|
43
|
+
views_dir = File.join(workspace_root, "app", "views", plural)
|
|
44
|
+
if File.directory?(views_dir)
|
|
45
|
+
children = dir_children(workspace_root, "app/views/#{plural}")
|
|
46
|
+
result[:views] = children unless children.empty?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# ── helper ────────────────────────────────────────────────────────────────
|
|
50
|
+
helper_path = "app/helpers/#{plural}_helper.rb"
|
|
51
|
+
if file_exists?(workspace_root, helper_path)
|
|
52
|
+
result[:helper] = [entry(helper_path)]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# ── tests ─────────────────────────────────────────────────────────────────
|
|
56
|
+
test_candidates = [
|
|
57
|
+
"test/controllers/#{plural}_controller_test.rb",
|
|
58
|
+
"test/models/#{singular}_test.rb",
|
|
59
|
+
"spec/controllers/#{plural}_controller_spec.rb",
|
|
60
|
+
"spec/models/#{singular}_spec.rb"
|
|
61
|
+
]
|
|
62
|
+
tests = test_candidates.select { |p| file_exists?(workspace_root, p) }.map { |p| entry(p) }
|
|
63
|
+
result[:tests] = tests unless tests.empty?
|
|
64
|
+
|
|
65
|
+
# ── custom paths ──────────────────────────────────────────────────────────
|
|
66
|
+
custom_result = {}
|
|
67
|
+
Array(custom_paths).each do |base|
|
|
68
|
+
base = base.to_s.strip
|
|
69
|
+
next if base.empty?
|
|
70
|
+
|
|
71
|
+
[plural, singular].uniq.each do |name|
|
|
72
|
+
rel_dir = "#{base}/#{name}"
|
|
73
|
+
abs_dir = File.join(workspace_root, rel_dir)
|
|
74
|
+
next unless File.directory?(abs_dir)
|
|
75
|
+
|
|
76
|
+
begin
|
|
77
|
+
real_ws = File.realpath(workspace_root)
|
|
78
|
+
real_dir = File.realpath(abs_dir)
|
|
79
|
+
next unless real_dir.start_with?("#{real_ws}/") || real_dir == real_ws
|
|
80
|
+
rescue Errno::ENOENT, Errno::EACCES
|
|
81
|
+
next
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
children = dir_children(workspace_root, rel_dir)
|
|
85
|
+
next if children.empty?
|
|
86
|
+
|
|
87
|
+
custom_result[base] ||= []
|
|
88
|
+
custom_result[base].concat(children)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
result[:custom] = custom_result unless custom_result.empty?
|
|
92
|
+
|
|
93
|
+
result
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# Helpers
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
# Returns [plural, singular] workspace-relative resource name strings
|
|
101
|
+
# (may include namespace prefix, e.g. "admin/users", "admin/user"),
|
|
102
|
+
# or nil when the path does not match a known Rails convention.
|
|
103
|
+
def extract_resource_names(relative_path)
|
|
104
|
+
parts = relative_path.to_s.split("/")
|
|
105
|
+
return nil unless parts.length >= 2
|
|
106
|
+
|
|
107
|
+
case parts[0]
|
|
108
|
+
when "app"
|
|
109
|
+
case parts[1]
|
|
110
|
+
when "controllers"
|
|
111
|
+
# app/controllers/[namespace/]name_controller.rb
|
|
112
|
+
return nil unless parts.last.end_with?("_controller.rb")
|
|
113
|
+
|
|
114
|
+
ns_and_file = parts[2..]
|
|
115
|
+
file = ns_and_file.last
|
|
116
|
+
ns = ns_and_file[0..-2] # may be empty
|
|
117
|
+
base = file.delete_suffix("_controller.rb")
|
|
118
|
+
plural = (ns + [base]).join("/")
|
|
119
|
+
singular = (ns + [singularize(base)]).join("/")
|
|
120
|
+
[plural, singular]
|
|
121
|
+
|
|
122
|
+
when "models"
|
|
123
|
+
# app/models/[namespace/]name.rb
|
|
124
|
+
return nil unless parts.last.end_with?(".rb")
|
|
125
|
+
|
|
126
|
+
ns_and_file = parts[2..]
|
|
127
|
+
file = ns_and_file.last.delete_suffix(".rb")
|
|
128
|
+
ns = ns_and_file[0..-2]
|
|
129
|
+
singular = (ns + [file]).join("/")
|
|
130
|
+
plural = (ns + [pluralize(file)]).join("/")
|
|
131
|
+
[plural, singular]
|
|
132
|
+
|
|
133
|
+
when "views"
|
|
134
|
+
# app/views/[namespace/]resource/anything
|
|
135
|
+
return nil unless parts.length >= 4
|
|
136
|
+
|
|
137
|
+
# All path segments between "views" and the view filename form the resource path.
|
|
138
|
+
# e.g. app/views/admin/users/index.html.erb → view_resource = ["admin","users"]
|
|
139
|
+
# e.g. app/views/users/index.html.erb → view_resource = ["users"]
|
|
140
|
+
view_resource = parts[2..parts.length - 2]
|
|
141
|
+
plural = view_resource.join("/")
|
|
142
|
+
singular = (view_resource[0..-2] + [singularize(view_resource.last)]).join("/")
|
|
143
|
+
[plural, singular]
|
|
144
|
+
|
|
145
|
+
when "helpers"
|
|
146
|
+
# app/helpers/[namespace/]name_helper.rb
|
|
147
|
+
return nil unless parts.last.end_with?("_helper.rb")
|
|
148
|
+
|
|
149
|
+
ns_and_file = parts[2..]
|
|
150
|
+
file = ns_and_file.last
|
|
151
|
+
ns = ns_and_file[0..-2]
|
|
152
|
+
base = file.delete_suffix("_helper.rb")
|
|
153
|
+
plural = (ns + [base]).join("/")
|
|
154
|
+
singular = (ns + [singularize(base)]).join("/")
|
|
155
|
+
[plural, singular]
|
|
156
|
+
|
|
157
|
+
else
|
|
158
|
+
nil
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
when "test", "spec"
|
|
162
|
+
framework = parts[0]
|
|
163
|
+
case parts[1]
|
|
164
|
+
when "controllers"
|
|
165
|
+
# test/controllers/[ns/]name_controller_test.rb
|
|
166
|
+
suffix = framework == "test" ? "_controller_test.rb" : "_controller_spec.rb"
|
|
167
|
+
return nil unless parts.last.end_with?(suffix)
|
|
168
|
+
|
|
169
|
+
ns_and_file = parts[2..]
|
|
170
|
+
file = ns_and_file.last
|
|
171
|
+
ns = ns_and_file[0..-2]
|
|
172
|
+
base = file.delete_suffix(suffix)
|
|
173
|
+
plural = (ns + [base]).join("/")
|
|
174
|
+
singular = (ns + [singularize(base)]).join("/")
|
|
175
|
+
[plural, singular]
|
|
176
|
+
|
|
177
|
+
when "models"
|
|
178
|
+
# test/models/[ns/]name_test.rb
|
|
179
|
+
suffix = framework == "test" ? "_test.rb" : "_spec.rb"
|
|
180
|
+
return nil unless parts.last.end_with?(suffix)
|
|
181
|
+
|
|
182
|
+
ns_and_file = parts[2..]
|
|
183
|
+
file = ns_and_file.last
|
|
184
|
+
ns = ns_and_file[0..-2]
|
|
185
|
+
base = file.delete_suffix(suffix)
|
|
186
|
+
singular = (ns + [base]).join("/")
|
|
187
|
+
plural = (ns + [pluralize(base)]).join("/")
|
|
188
|
+
[plural, singular]
|
|
189
|
+
|
|
190
|
+
else
|
|
191
|
+
nil
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
else
|
|
195
|
+
nil
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def entry(rel_path)
|
|
200
|
+
{ path: rel_path, name: File.basename(rel_path), type: :file }
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def file_exists?(workspace_root, rel_path)
|
|
204
|
+
File.exist?(File.join(workspace_root, rel_path))
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Returns direct file children (not subdirectories) of a workspace-relative
|
|
208
|
+
# directory, sorted by name, as {path:, name:, type: :file} hashes.
|
|
209
|
+
def dir_children(workspace_root, rel_dir)
|
|
210
|
+
abs_dir = File.join(workspace_root, rel_dir)
|
|
211
|
+
return [] unless File.directory?(abs_dir)
|
|
212
|
+
|
|
213
|
+
Dir.entries(abs_dir)
|
|
214
|
+
.reject { |n| n.start_with?(".") }
|
|
215
|
+
.sort
|
|
216
|
+
.filter_map do |name|
|
|
217
|
+
full = File.join(abs_dir, name)
|
|
218
|
+
next unless File.file?(full)
|
|
219
|
+
|
|
220
|
+
{ path: "#{rel_dir}/#{name}", name: name, type: :file }
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def singularize(word)
|
|
225
|
+
ActiveSupport::Inflector.singularize(word)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def pluralize(word)
|
|
229
|
+
ActiveSupport::Inflector.pluralize(word)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
private_class_method :entry, :file_exists?, :dir_children, :singularize, :pluralize, :extract_resource_names
|
|
233
|
+
end
|
|
234
|
+
end
|
data/config/routes.rb
CHANGED
|
@@ -26,6 +26,7 @@ Mbeditor::Engine.routes.draw do
|
|
|
26
26
|
get 'module_members', to: 'editors#module_members'
|
|
27
27
|
get 'file_includes', to: 'editors#file_includes'
|
|
28
28
|
get 'unused_methods', to: 'editors#unused_methods'
|
|
29
|
+
get 'related_files', to: 'editors#related_files'
|
|
29
30
|
get 'git_info', to: 'editors#git_info'
|
|
30
31
|
get 'git_status', to: 'editors#git_status'
|
|
31
32
|
get 'manifest.webmanifest', to: 'editors#pwa_manifest', format: false
|
|
@@ -7,7 +7,7 @@ module Mbeditor
|
|
|
7
7
|
:test_framework, :test_command, :test_timeout,
|
|
8
8
|
:authenticate_with,
|
|
9
9
|
:lint_timeout, :base_branch_candidates, :git_timeout,
|
|
10
|
-
:ruby_def_include_dirs
|
|
10
|
+
:ruby_def_include_dirs, :related_files_custom_paths
|
|
11
11
|
|
|
12
12
|
def initialize
|
|
13
13
|
@allowed_environments = [:development]
|
|
@@ -25,6 +25,7 @@ module Mbeditor
|
|
|
25
25
|
@base_branch_candidates = %w[origin/develop origin/main origin/master develop main master]
|
|
26
26
|
@git_timeout = nil # seconds; nil disables (no timeout on git subprocesses)
|
|
27
27
|
@ruby_def_include_dirs = %w[app/models app/controllers app/helpers app/concerns]
|
|
28
|
+
@related_files_custom_paths = []
|
|
28
29
|
end
|
|
29
30
|
end
|
|
30
31
|
end
|
data/lib/mbeditor/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mbeditor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oliver Noonan
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -94,6 +94,7 @@ files:
|
|
|
94
94
|
- app/services/mbeditor/git_service.rb
|
|
95
95
|
- app/services/mbeditor/js_definition_service.rb
|
|
96
96
|
- app/services/mbeditor/js_members_service.rb
|
|
97
|
+
- app/services/mbeditor/rails_related_files_service.rb
|
|
97
98
|
- app/services/mbeditor/redmine_service.rb
|
|
98
99
|
- app/services/mbeditor/ri_definition_service.rb
|
|
99
100
|
- app/services/mbeditor/ruby_definition_service.rb
|