mbeditor 0.12.0 → 0.12.2

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.
@@ -117,6 +117,14 @@ var FileService = (function () {
117
117
  return axios.get(window.mbeditorBasePath() + '/ping', { timeout: 4000 }).then(function(res) { return res.data; });
118
118
  }
119
119
 
120
+ // Which routes reach each action in a controller. Returns { controller, actions }
121
+ // with an empty actions map for anything that is not a controller, so the
122
+ // caller does not have to know the naming convention.
123
+ function getRoutes(path) {
124
+ return axios.get(window.mbeditorBasePath() + '/routes?path=' + encodeURIComponent(path))
125
+ .then(function (res) { return res.data; });
126
+ }
127
+
120
128
  function getState() {
121
129
  return axios.get(window.mbeditorBasePath() + '/state').then(function(res) { return res.data; });
122
130
  }
@@ -336,6 +344,7 @@ var FileService = (function () {
336
344
  formatFile: formatFile,
337
345
  runTests: runTests,
338
346
  ping: ping,
347
+ getRoutes: getRoutes,
339
348
  getState: getState,
340
349
  saveState: saveState,
341
350
  getBranchState: getBranchState,
@@ -61,6 +61,17 @@ html, body, #mbeditor-root {
61
61
  font-size: 13px;
62
62
  color: var(--ide-text);
63
63
  font-weight: 500;
64
+ /* Never wrap: the titlebar is a fixed 32px row, so a second line pushed the
65
+ toolbar buttons out of it. */
66
+ white-space: nowrap;
67
+ }
68
+
69
+ /* Below the width where the toolbar drops its button labels, drop the title
70
+ too. The layers icon beside it already identifies the editor, and the host
71
+ and port are in the browser's own address bar. */
72
+ @media (max-width: 1180px) {
73
+ .ide-titlebar-title { display: none; }
74
+ .ide-titlebar-icon { margin-right: 0; }
64
75
  }
65
76
 
66
77
  /* Fills the gap between the title and the button cluster. min-width: 0 lets it
@@ -3120,8 +3131,16 @@ button:not(.pico-btn) { margin-bottom: 0; }
3120
3131
  margin-left: auto;
3121
3132
  }
3122
3133
  .ide-model-graph-search {
3123
- width: 150px;
3124
- padding: 3px 6px;
3134
+ width: 170px;
3135
+ /* Two separate things drew over the text here. WebKit renders its own
3136
+ magnifier and clear button inside type="search"; and the vendored Pico
3137
+ stylesheet paints a magnifier as a background-image on every [type=search],
3138
+ positioned at the left, which the 8px padding put the text straight on top
3139
+ of. Disabling the native decorations alone fixed neither — the visible icon
3140
+ was Pico's. Both are off; the placeholder already says what the field is. */
3141
+ -webkit-appearance: none;
3142
+ appearance: none;
3143
+ padding: 3px 8px;
3125
3144
  background: var(--ide-input-bg, #3c3c3c);
3126
3145
  border: 1px solid var(--ide-border, #3c3c3c);
3127
3146
  border-radius: 3px;
@@ -3243,3 +3262,136 @@ button:not(.pico-btn) { margin-bottom: 0; }
3243
3262
  .statusbar-problems-error-icon { color: var(--ide-danger); }
3244
3263
  .statusbar-problems-warning-icon { color: var(--ide-warning); }
3245
3264
  .statusbar-problems-count { font-variant-numeric: tabular-nums; }
3265
+
3266
+ /* Search decorations off across engines — Chrome/Safari draw their own
3267
+ magnifier and clear button inside type="search". */
3268
+ .ide-model-graph-search::-webkit-search-decoration,
3269
+ .ide-model-graph-search::-webkit-search-cancel-button,
3270
+ .ide-model-graph-search::-webkit-search-results-button,
3271
+ .ide-model-graph-search::-webkit-search-results-decoration {
3272
+ -webkit-appearance: none;
3273
+ appearance: none;
3274
+ }
3275
+
3276
+ /* Cluster areas — a connected part of the schema, drawn behind its models so
3277
+ the grouping is visible rather than implied by proximity alone. */
3278
+ .mg-region-box {
3279
+ fill: color-mix(in srgb, var(--ide-fg, #d4d4d4) 4%, transparent);
3280
+ stroke: color-mix(in srgb, var(--ide-fg, #d4d4d4) 12%, transparent);
3281
+ stroke-width: 1;
3282
+ }
3283
+ .mg-region-label {
3284
+ fill: color-mix(in srgb, var(--ide-fg, #d4d4d4) 45%, transparent);
3285
+ font-size: 11px;
3286
+ font-weight: 500;
3287
+ pointer-events: none;
3288
+ }
3289
+
3290
+ /* Hovering a model dims every association it is not part of. With 500+ edges on
3291
+ screen the lines are otherwise indistinguishable from each other, and picking
3292
+ out one model's relationships is the whole reason to look at the diagram.
3293
+ Applied by toggling classes on the DOM rather than through React — re-rendering
3294
+ thousands of SVG nodes on mouseenter is what made this view unusable before. */
3295
+ /* The ~30 ms this costs on a 300-model graph is the browser restyling every
3296
+ edge and box, not anything in our code — toggling the classes themselves
3297
+ measures 1.6 ms. Switching to stroke-opacity/fill-opacity was tried on the
3298
+ theory that opacity stacking contexts were the cost; measured slower (38 ms
3299
+ vs 32 ms), so plain opacity stays. */
3300
+ .mg-focus-mode .mg-edge { opacity: 0.08; }
3301
+ .mg-focus-mode .mg-lit .mg-edge { opacity: 1; stroke-width: 2; }
3302
+ .mg-focus-mode .mg-node { opacity: 0.55; }
3303
+ .mg-focus-mode .mg-node:hover { opacity: 1; }
3304
+
3305
+ /* The association list shown while a model is hovered. Kept mounted and toggled
3306
+ with a class so moving between boxes doesn't thrash the DOM. */
3307
+ .mg-assoc-card {
3308
+ position: absolute;
3309
+ z-index: 30;
3310
+ max-width: 290px;
3311
+ padding: 8px 10px;
3312
+ background: var(--ide-panel-bg, #252526);
3313
+ border: 1px solid var(--ide-border, #3c3c3c);
3314
+ border-radius: 6px;
3315
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45);
3316
+ font-size: 11px;
3317
+ line-height: 1.5;
3318
+ color: var(--ide-text, #d4d4d4);
3319
+ pointer-events: none;
3320
+ }
3321
+ .mg-assoc-card-hidden { display: none; }
3322
+ .mg-assoc-title { font-weight: 600; margin-bottom: 4px; }
3323
+ .mg-assoc-count { color: var(--ide-text-muted, #858585); margin-bottom: 4px; }
3324
+ .mg-assoc-row { display: flex; align-items: baseline; gap: 5px; white-space: nowrap; }
3325
+ .mg-assoc-dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex-shrink: 0; }
3326
+ .mg-assoc-macro { color: var(--ide-accent-fg, #4ec9b0); }
3327
+ .mg-assoc-name { color: var(--ide-text, #d4d4d4); }
3328
+ .mg-assoc-dir { color: var(--ide-text-muted, #858585); overflow: hidden; text-overflow: ellipsis; }
3329
+ .mg-assoc-through { color: var(--ide-text-muted, #858585); font-style: italic; }
3330
+ .mg-assoc-more, .mg-assoc-empty { color: var(--ide-text-muted, #858585); margin-top: 3px; }
3331
+
3332
+ /* Explicit "open the schema" target in each model's header. */
3333
+ .mg-open-btn { cursor: pointer; }
3334
+ .mg-open-btn-bg { fill: transparent; }
3335
+ .mg-open-btn-bar { fill: var(--ide-text-muted, #858585); }
3336
+ .mg-open-btn:hover .mg-open-btn-bg { fill: color-mix(in srgb, var(--ide-fg, #d4d4d4) 16%, transparent); }
3337
+ .mg-open-btn:hover .mg-open-btn-bar { fill: var(--ide-fg, #d4d4d4); }
3338
+ .mg-node { cursor: zoom-in; }
3339
+
3340
+ /* Pico paints a magnifier as a background-image on every [type=search], at the
3341
+ left of the field, and our 8px padding put the text straight on top of it.
3342
+ Its selector is input:not([type=checkbox],...)[type=search] — specificity
3343
+ (0,2,1) — so a plain .class rule (0,1,0) loses and the icon stayed. Matching
3344
+ that specificity, and loading later, is what actually removes it. */
3345
+ input.ide-model-graph-search[type="search"] {
3346
+ background-image: none;
3347
+ padding-inline-start: 8px;
3348
+ }
3349
+
3350
+ /* Model-graph search dropdown. Replaces a native <datalist>, which the browser
3351
+ renders in its own chrome — unstyleable, and unable to show the table name
3352
+ beside each model. */
3353
+ .mg-search-wrap { position: relative; display: inline-block; }
3354
+ .mg-search-list {
3355
+ position: absolute;
3356
+ top: calc(100% + 4px);
3357
+ right: 0;
3358
+ z-index: 40;
3359
+ min-width: 220px;
3360
+ max-height: 280px;
3361
+ overflow-y: auto;
3362
+ margin: 0;
3363
+ padding: 4px 0;
3364
+ list-style: none;
3365
+ background: var(--ide-panel-bg, #252526);
3366
+ border: 1px solid var(--ide-border, #3c3c3c);
3367
+ border-radius: 6px;
3368
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45);
3369
+ }
3370
+ .mg-search-option {
3371
+ display: flex;
3372
+ align-items: baseline;
3373
+ justify-content: space-between;
3374
+ gap: 10px;
3375
+ padding: 4px 10px;
3376
+ font-size: 11px;
3377
+ color: var(--ide-text, #d4d4d4);
3378
+ cursor: pointer;
3379
+ white-space: nowrap;
3380
+ }
3381
+ .mg-search-option-active { background: var(--ide-accent, #3794ff); color: #fff; }
3382
+ .mg-search-option-name { font-family: var(--ide-mono, monospace); }
3383
+ .mg-search-option-meta { color: var(--ide-text-muted, #858585); font-size: 10px; }
3384
+ .mg-search-option-active .mg-search-option-meta { color: rgba(255, 255, 255, 0.75); }
3385
+
3386
+ /* Inline route hints beside controller actions. Muted and italic so they read
3387
+ as annotation rather than code — they sit on the same line as the `def`. */
3388
+ .mbeditor-route-hint {
3389
+ color: var(--ide-text-muted, #858585) !important;
3390
+ font-style: italic;
3391
+ opacity: 0.85;
3392
+ }
3393
+ .mbeditor-route-hint-none {
3394
+ color: var(--ide-warning, #cca700) !important;
3395
+ font-style: italic;
3396
+ opacity: 0.8;
3397
+ }
@@ -1072,6 +1072,19 @@ module Mbeditor
1072
1072
  }
1073
1073
  end
1074
1074
 
1075
+ # GET /mbeditor/routes?path=app/controllers/users_controller.rb
1076
+ #
1077
+ # Which routes reach each action in a controller, for the inline hints the
1078
+ # editor draws beside every `def`. Returns {} for anything that is not a
1079
+ # controller rather than erroring — the client asks for every Ruby file it
1080
+ # opens and should not have to know the convention.
1081
+ def routes
1082
+ key = RouteService.controller_key(params[:path].to_s)
1083
+ return render json: { controller: nil, actions: {} } unless key
1084
+
1085
+ render json: { controller: key, actions: RouteService.for_controller(key) }
1086
+ end
1087
+
1075
1088
  # GET /mbeditor/related_files?path=...
1076
1089
  def related_files
1077
1090
  path = resolve_path(params[:path])
@@ -1145,14 +1158,41 @@ module Mbeditor
1145
1158
  # authenticate_with) so the host app can supply a collaboration display name.
1146
1159
  # nil — including any failure or a blank result — falls through to the
1147
1160
  # client-generated name.
1161
+ # Display name for the collaboration caret. An explicit user_name_callback
1162
+ # wins; otherwise fall back to current_user, when the host's auth library put
1163
+ # it within reach of an ActionController::Base subclass (Devise, Sorcery and
1164
+ # friends do; a current_user hand-written on the host's own
1165
+ # ApplicationController does not — see default_user_name).
1148
1166
  def resolved_user_name
1149
1167
  cb = Mbeditor.configuration.user_name_callback
1150
- return nil unless cb
1151
-
1152
- name = instance_exec(&cb)
1168
+ name = cb ? instance_exec(&cb) : default_user_name
1153
1169
  name = name.to_s.strip
1154
1170
  name.empty? ? nil : name
1155
- rescue StandardError
1171
+ rescue StandardError => e
1172
+ # Was a bare `nil`, which made a broken callback indistinguishable from an
1173
+ # unconfigured one: you got the generated name and no clue why. The usual
1174
+ # cause is a NameError on current_user, and silently swallowing it sent
1175
+ # people looking in the wrong place.
1176
+ Rails.logger.warn("[mbeditor] user name lookup failed: #{e.class}: #{e.message}")
1177
+ nil
1178
+ end
1179
+
1180
+ # Read the display name straight off current_user, trying each configured
1181
+ # attribute in turn. This is what makes an authenticated editor show real
1182
+ # names with no extra wiring; user_name_methods exists so an app whose column
1183
+ # isn't in the default list can name it instead of writing a callback.
1184
+ def default_user_name
1185
+ return nil unless respond_to?(:current_user, true)
1186
+
1187
+ user = current_user
1188
+ return nil unless user
1189
+
1190
+ Array(Mbeditor.configuration.user_name_methods).each do |m|
1191
+ next unless user.respond_to?(m)
1192
+
1193
+ value = user.public_send(m).to_s.strip
1194
+ return value unless value.empty?
1195
+ end
1156
1196
  nil
1157
1197
  end
1158
1198
 
@@ -22,7 +22,16 @@ module Mbeditor
22
22
  SolidQueue SolidCache SolidCable
23
23
  ].freeze
24
24
 
25
- MAX_MODELS = 300
25
+ # Cap on models drawn. 300 was chosen before the layout could handle that
26
+ # many; a schema slightly over it silently lost models and only said
27
+ # "(truncated)". Raised, and configurable for anything larger — the cost of
28
+ # a bigger graph is now the browser's rendering, not the layout.
29
+ DEFAULT_MAX_MODELS = 1000
30
+
31
+ def self.max_models
32
+ value = Mbeditor.configuration.model_graph_max_models.to_i
33
+ value.positive? ? value : DEFAULT_MAX_MODELS
34
+ end
26
35
 
27
36
  # Only the first few columns travel: the diagram box shows that many and the
28
37
  # full list is a click away in the schema modal, which fetches its own data
@@ -86,7 +95,8 @@ module Mbeditor
86
95
  classes = model_classes
87
96
  return unavailable("No ActiveRecord models found in this application.") if classes.empty?
88
97
 
89
- models = classes.first(MAX_MODELS).map { |klass| describe_model(klass, root) }
98
+ limit = max_models
99
+ models = classes.first(limit).map { |klass| describe_model(klass, root) }
90
100
  known = models.map { |m| m[:name] }.to_set
91
101
 
92
102
  {
@@ -95,7 +105,7 @@ module Mbeditor
95
105
  # Edges to classes outside the graph (a gem's model, or a typo in
96
106
  # class_name:) would render as arrows into nowhere.
97
107
  edges: models.flat_map { |m| m.delete(:edges) }.select { |e| known.include?(e[:to]) }.uniq,
98
- truncated: classes.length > MAX_MODELS,
108
+ truncated: classes.length > limit,
99
109
  generatedAt: Time.now.utc.iso8601
100
110
  }
101
111
  rescue StandardError => e
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mbeditor
4
+ # Which routes reach a controller's actions, for the inline hints shown beside
5
+ # each `def` in a controller file.
6
+ #
7
+ # Read from the host app's own route set rather than by parsing config/routes.rb.
8
+ # mbeditor runs inside the host app, so the routes are already built — and they
9
+ # are the only source that accounts for `resources` expansion, `member`/
10
+ # `collection` blocks, scopes, constraints, mounted engines and anything a
11
+ # routes file does in plain Ruby. Parsing the file would get all of that wrong.
12
+ module RouteService
13
+ module_function
14
+
15
+ # Actions with no route are worth calling out, but only for controllers Rails
16
+ # would actually dispatch to. These are the inherited ones every controller
17
+ # has and nobody routes.
18
+ NON_ACTION_METHODS = %w[
19
+ new inspect method_of to_s hash class dup freeze
20
+ ].freeze
21
+
22
+ # "app/controllers/admin/users_controller.rb" -> "admin/users", which is the
23
+ # key Rails stores in a route's defaults.
24
+ def controller_key(relative_path)
25
+ path = relative_path.to_s.sub(%r{\A/+}, "")
26
+ match = path.match(%r{\Aapp/controllers/(.+)_controller\.rb\z})
27
+ return nil unless match
28
+
29
+ match[1]
30
+ end
31
+
32
+ # => { "show" => [{ verb:, path:, name: }], ... }
33
+ def for_controller(key)
34
+ return {} if key.nil? || key.empty?
35
+ return {} unless defined?(Rails) && Rails.respond_to?(:application) && Rails.application
36
+
37
+ routes_for(key)
38
+ rescue StandardError
39
+ # A broken route set must not take the editor down with it — the file still
40
+ # opens, just without hints.
41
+ {}
42
+ end
43
+
44
+ def routes_for(key)
45
+ out = {}
46
+ Rails.application.routes.routes.each do |route|
47
+ defaults = route.defaults
48
+ next unless defaults[:controller] == key
49
+
50
+ action = defaults[:action].to_s
51
+ next if action.empty?
52
+
53
+ (out[action] ||= []) << {
54
+ verb: verb_for(route),
55
+ path: path_for(route),
56
+ name: route.name
57
+ }
58
+ end
59
+ out
60
+ end
61
+ private_class_method :routes_for
62
+
63
+ # Rails has moved this between a String and a Regexp across versions; both
64
+ # stringify usefully, but a Regexp needs its slashes stripped.
65
+ def verb_for(route)
66
+ verb = route.verb
67
+ verb = verb.source.gsub(%r{[$^]}, "") if verb.is_a?(Regexp)
68
+ verb = verb.to_s
69
+ verb.empty? ? "ANY" : verb
70
+ end
71
+ private_class_method :verb_for
72
+
73
+ # "(.:format)" is noise in a hint that has to fit on one line.
74
+ def path_for(route)
75
+ route.path.spec.to_s.sub(/\(\.:format\)\z/, "")
76
+ end
77
+ private_class_method :path_for
78
+ end
79
+ end
@@ -5,14 +5,14 @@ module Mbeditor
5
5
  attr_accessor :allowed_environments, :workspace_root, :excluded_paths, :rubocop_command, :rubocop_server,
6
6
  :redmine_enabled, :redmine_url, :redmine_api_key, :redmine_ticket_source,
7
7
  :test_framework, :test_command, :test_timeout,
8
- :authenticate_with, :authentication_cache_ttl, :user_name_callback,
8
+ :authenticate_with, :authentication_cache_ttl, :user_name_callback, :user_name_methods,
9
9
  :lint_timeout, :base_branch_candidates, :git_timeout, :search_timeout,
10
10
  :ruby_def_include_dirs, :related_files_custom_paths,
11
11
  :mount_path, :resilient_routing, :js_global_identifiers,
12
12
  :js_program, :js_program_exclude,
13
13
  :js_syntax_check, :babel_standalone_path,
14
14
  :ruby_lsp, :ruby_lsp_command, :ruby_lsp_timeout,
15
- :exception_capture,
15
+ :exception_capture, :model_graph_max_models,
16
16
  :search_respect_gitignore, :ripgrep_command
17
17
 
18
18
  def initialize
@@ -58,7 +58,11 @@ module Mbeditor
58
58
  @ruby_def_include_dirs = %w[app/models app/controllers app/helpers app/concerns]
59
59
  @related_files_custom_paths = []
60
60
  @authentication_cache_ttl = 0
61
- @user_name_callback = nil # proc resolved in controller context (instance_exec) → collaboration display name; nil falls through to the client-generated name
61
+ @user_name_callback = nil # proc resolved in controller context (instance_exec) → collaboration display name; nil falls through to current_user, then to the client-generated name
62
+ # Attributes tried on current_user, in order, when no user_name_callback
63
+ # is set. First non-blank one wins. Name your own column here rather than
64
+ # writing a callback for the common case.
65
+ @user_name_methods = %w[name full_name display_name username login email]
62
66
  @js_global_identifiers = [] # extra ambient JS globals for the editor (runtime-only names invisible to static scan, e.g. %w[Routes I18n])
63
67
  # Load the workspace's own JS source into Monaco's TypeScript program, so
64
68
  # cross-file references get real inferred types instead of ambient `any`.
@@ -80,6 +84,9 @@ module Mbeditor
80
84
  # same exposure the log panel already has, since it tails the dev log.
81
85
  # Set to false to record nothing.
82
86
  @exception_capture = :auto
87
+ # Models drawn in the model graph before it reports itself truncated.
88
+ # nil uses ModelGraphService::DEFAULT_MAX_MODELS.
89
+ @model_graph_max_models = nil
83
90
  @mount_path = nil # explicit URL prefix override; nil falls through to detection/"/mbeditor"
84
91
  @resilient_routing = true # serve /mbeditor from middleware so the editor survives a broken host routes.rb; false is the escape hatch
85
92
  end
@@ -54,9 +54,23 @@ module Mbeditor
54
54
  # Insert before CheckPending so our middleware wraps it and can rescue
55
55
  # the error it raises. Falls back silently if CheckPending is absent
56
56
  # (e.g. host app does not use ActiveRecord).
57
- # Note: app.middleware is a MiddlewareStackProxy during initializers and
58
- # does not support .to_a rely solely on defined? to detect ActiveRecord.
59
- if defined?(ActiveRecord::Migration::CheckPending)
57
+ #
58
+ # The constant being defined is not enough: Rails only puts CheckPending
59
+ # in the stack when config.active_record.migration_error is :page_load, so
60
+ # an app that loads ActiveRecord with any other setting has the constant
61
+ # but no middleware — and insert_before then raises "No such middleware",
62
+ # taking the host app's boot down with it. app.middleware is a
63
+ # MiddlewareStackProxy here and cannot be inspected, so the only way to
64
+ # know is to try it. Losing this middleware costs a friendlier pending-
65
+ # migration page, which is never worth failing to boot over.
66
+ # Rescuing the call is not an option: app.middleware is a
67
+ # MiddlewareStackProxy, so insert_before only records the operation and the
68
+ # "No such middleware" error surfaces later, during merge_into. Test the
69
+ # same condition Rails uses to add CheckPending in the first place.
70
+ migration_error = app.config.respond_to?(:active_record) &&
71
+ app.config.active_record[:migration_error]
72
+
73
+ if defined?(ActiveRecord::Migration::CheckPending) && migration_error == :page_load
60
74
  app.middleware.insert_before ActiveRecord::Migration::CheckPending,
61
75
  Mbeditor::Rack::HandlePendingMigrations
62
76
  end
@@ -44,6 +44,7 @@ module Mbeditor
44
44
  get 'client_config', to: 'editors#client_config'
45
45
  get 'related_files', to: 'editors#related_files'
46
46
  get 'model_schema', to: 'editors#model_schema'
47
+ get 'routes', to: 'editors#routes'
47
48
  get 'changelog', to: 'editors#changelog'
48
49
  get 'git_info', to: 'editors#git_info'
49
50
  get 'git_status', to: 'editors#git_status'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mbeditor
4
- VERSION = "0.12.0"
4
+ VERSION = "0.12.2"
5
5
  end
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.12.0
4
+ version: 0.12.2
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-07-31 00:00:00.000000000 Z
11
+ date: 2026-08-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -133,6 +133,7 @@ files:
133
133
  - app/services/mbeditor/rails_related_files_service.rb
134
134
  - app/services/mbeditor/redmine_service.rb
135
135
  - app/services/mbeditor/ri_definition_service.rb
136
+ - app/services/mbeditor/route_service.rb
136
137
  - app/services/mbeditor/ruby_definition_service.rb
137
138
  - app/services/mbeditor/safe_path.rb
138
139
  - app/services/mbeditor/schema_service.rb