mbeditor
Mbeditor (Mini Browser Editor) is a mountable Rails engine that adds a browser-based editor UI to a Rails app.
Features
- Two-pane tabbed editor with drag-to-move tabs
- File tree and project search
- Git panel with working tree changes, unpushed file changes, and branch commit titles
- Optional RuboCop lint and format endpoints (uses host app RuboCop)
- Optional Ruby language-server integration (definitions, hover, completion, diagnostics)
- Optional test runner with inline failure markers and a dedicated results panel (Minitest and RSpec)
Security Warning
Mbeditor exposes read and write access to your Rails application directory over HTTP. It is intended only for local development.
- Never install mbeditor in production or staging.
- Never run it on infrastructure accessible to untrusted users.
- Always keep it in the development group in your Gemfile.
- The engine enforces environment restrictions at runtime, and Gemfile scoping is a second line of defense that keeps the gem out of deploy builds.
Installation
- Add the gem to the host app Gemfile in development only:
gem 'mbeditor', group: :development
- Install dependencies:
bundle install
- Mount the engine in host app routes:
mount Mbeditor::Engine, at: "/mbeditor"
-
Create
config/initializers/mbeditor.rbusing the configuration options below. -
Boot your app and open
/mbeditor.
Configuration
Use a single initializer to set the engine options you need:
Mbeditor.configure do |config|
config.allowed_environments = [:development]
# config.workspace_root = Rails.root
config.excluded_paths = %w[.git tmp log node_modules .bundle coverage vendor/bundle]
config.rubocop_command = "bundle exec rubocop"
# Optional authentication (runs as a before_action in the engine controllers)
# config.authenticate_with = proc { redirect_to login_path unless UserSession.find }
# Optional test runner (Minitest or RSpec)
# config.test_framework = :minitest # :minitest or :rspec — auto-detected when nil
# config.test_command = "bundle exec rails test" # defaults to bin/rails test or bundle exec ruby -Itest
# config.test_timeout = 60
# Optional Redmine integration
# config.redmine_enabled = true
# config.redmine_url = "https://redmine.example.com/"
# config.redmine_api_key = "optional_api_key_override"
# config.redmine_ticket_source = :commit # :commit (scan recent commit messages for #123) or :branch (leading digits of branch name)
# Optional Ruby/Rails side panel tuning
# config.ruby_def_include_dirs = %w[app/models app/controllers app/helpers app/concerns]
# config.related_files_custom_paths = %w[app/assets/javascripts/app app/policies]
# Resilient routing (see the "Resilient Routing" section below)
# config.mount_path = "/mbeditor" # explicit prefix override; auto-detected when nil
# config.resilient_routing = false # escape hatch; true keeps the editor up when host routes break
end
Core
| Option | Default | Description |
|---|---|---|
allowed_environments |
[:development] |
Rails environments allowed to access the engine. |
workspace_root |
Rails.root |
Root directory exposed by Mbeditor. |
excluded_paths |
%w[.git tmp log node_modules .bundle coverage vendor/bundle public/assets storage] |
Files/directories hidden from the tree and path operations. Entries without / match a name anywhere in the path; entries with / match relative paths and their descendants. |
rubocop_command |
"rubocop" |
Command used for inline Ruby linting and formatting. |
git_timeout |
10 |
Seconds each git subprocess may run; a timed-out call degrades its own field of the git panel instead of failing the request. nil disables the bound. |
search_timeout |
15 |
Wall-clock bound on project-search subprocesses; a tripped deadline returns the partial results collected so far. nil disables. |
search_respect_gitignore |
false |
When true, project search and definition lookups skip files ignored by .gitignore. The default searches them, matching the editor's "show me everything on disk" behaviour. |
js_global_identifiers |
[] |
Extra JS names declared as ambient globals in the editor — for runtime-only globals the static workspace scan can't see (e.g. %w[Routes I18n]). |
js_syntax_check |
:auto |
Save-time babel parse check for JS/JSX using the host's mini_racer + babel-standalone (auto-detected; no-op when either is absent). false disables. |
babel_standalone_path |
nil |
Explicit path to the babel-standalone bundle for the syntax check; nil looks up babel.min.js/babel.js in the host's asset pipeline. |
ruby_lsp |
:auto |
Use the host's ruby-lsp for Ruby go-to-definition, hover, completion, and diagnostics when it's installed (a persistent process is managed per workspace). false disables. Without ruby-lsp everything degrades to the built-in grep/Ripper services — no behavior change. |
ruby_lsp_command |
nil |
Override the ruby-lsp launch command (String or Array). nil auto-resolves bin/ruby-lsp → installed gem → bundle exec ruby-lsp. |
ruby_lsp_timeout |
3 |
Seconds per LSP request; on timeout (e.g. during initial indexing) the editor falls back to the built-in services for that request. |
Authentication
| Option | Default | Description |
|---|---|---|
authenticate_with |
nil |
Proc run as a before_action in all engine controllers. Executed via instance_exec inside the controller, so it has access to session, cookies, redirect_to, and auth-library class methods (e.g. Authlogic's UserSession.find) — but not helper methods from the host's ApplicationController. |
authentication_cache_ttl |
0 |
Seconds to cache the auth result in the session (0 = no caching). Set e.g. 300 to avoid calling authenticate_with on every request when the proc is expensive. Trade-off: after host logout, mbeditor stays accessible for up to TTL seconds. |
Test runner
| Option | Default | Description |
|---|---|---|
test_framework |
nil |
:minitest or :rspec. Auto-detected from file suffix, .rspec, or test/spec directory when nil. |
test_command |
nil |
Full command used to run a test file. When nil, picks bin/rails test (Minitest) or bin/rspec / bundle exec rspec (RSpec). |
test_timeout |
60 |
Maximum seconds a test run may take before being killed. |
Redmine
| Option | Default | Description |
|---|---|---|
redmine_enabled |
false |
Enables issue-lookup integration. |
redmine_url |
— | Redmine base URL. Required when redmine_enabled is true. |
redmine_api_key |
— | Redmine API key. Required when redmine_enabled is true. |
redmine_ticket_source |
:commit |
How the current ticket is identified. :commit scans the 100 most recent branch commits for a #123 reference; :branch reads leading digits from the branch name (e.g. 123-my-feature → ticket 123). |
Ruby/Rails side panel
| Option | Default | Description |
|---|---|---|
ruby_def_include_dirs |
%w[app/models app/controllers app/helpers app/concerns] |
Workspace-relative directories searched when resolving Ruby go-to-definition jumps. Add any extra dirs holding Ruby source you want indexed. |
related_files_custom_paths |
[] |
Extra base directories for the Rails related-files side panel. For each entry the panel looks for a subdirectory named after the current resource (plural or singular). E.g. "app/assets/javascripts/app" surfaces app/assets/javascripts/app/users/ when editing a users resource. |
Resilient routing
See Resilient Routing for details.
| Option | Default | Description |
|---|---|---|
mount_path |
nil |
Explicit URL prefix to serve resilient routing from. When nil, auto-detected from your mount Mbeditor::Engine, at: "..." line on every healthy boot. Set only to override detection. |
resilient_routing |
true |
Keeps mbeditor reachable when the host's config/routes.rb is broken, by serving its traffic from middleware that dispatches to a private route set. Set to false as an escape hatch: no middleware is inserted and the private set is never built. |
Test Runner
The Test button appears in the editor toolbar for any .rb file when a test/ or spec/ directory exists in the workspace root. Clicking it:
- Resolves the active source file to its matching test file using standard Rails conventions (
app/models/user.rb→test/models/user_test.rb). If the open file is already a test file, it runs that file directly. - Runs the test file using the configured command in a subprocess with a timeout.
- Shows a Test Results panel with pass/fail counts, per-test status icons, and error messages.
- Optionally overlays inline failure markers in the Monaco editor (separate from RuboCop markers — the two never interfere). Use the marker icon in the panel header to toggle them.
Running a single test. With a test file open, Ctrl+Shift+T (or Run Test
at Cursor in the editor context menu) runs only the test enclosing the cursor.
The filter syntax follows the detected framework — path:line for RSpec and
bin/rails test, a -n name filter for the plain Minitest runner — so it works
with a custom test_command too. If the cursor isn't inside a test, or the open
file is a source file rather than its test, the whole file runs as usual.
Framework auto-detection order:
- File suffix:
_spec.rb→ RSpec,_test.rb→ Minitest .rspecfile present → RSpecspec/directory present → RSpectest/directory present → Minitest
Default commands (when test_command is not set):
- Minitest:
bin/rails test <file>ifbin/railsexists, otherwisebundle exec ruby -Itest <file> - RSpec:
bin/rspec <file>ifbin/rspecexists, otherwisebundle exec rspec --format json <file>
Resilient Routing
A broken config/routes.rb normally takes down the whole app — every request, including the one you'd use to fix it, raises while Rails tries to reload the route table. Mbeditor stays reachable anyway: it serves its own traffic from middleware that dispatches to a private route set built at boot and never registered with Rails' route reloader. When a bad route draw wipes the host route table, that private set survives the wipe, so /mbeditor keeps working while the rest of the app is down. This is on by default (resilient_routing = true); you don't need to configure anything.
Custom mount paths work with zero configuration. On every healthy boot, mbeditor detects the prefix from your mount Mbeditor::Engine, at: "..." line and remembers it. If the route table later breaks, the remembered prefix is used — so resilient routing serves from your custom mount, not just /mbeditor. Detection only ever updates from a healthy load, so a broken reload can never overwrite a good prefix. Set config.mount_path only if you want to override detection explicitly.
What it cannot recover from: a host app that fails to boot. Resilient routing relies on mbeditor's engine initializers running — that's when the middleware is inserted and the private route set is built. If the app can't boot at all, those initializers never run and there is nothing to fall back to. This includes:
- a syntax error or raised exception in
config/application.rbor other engine/initializer code, - a broken initializer in
config/initializers/, - a bad or incompatible gem that fails to load.
A broken route is recoverable in the browser; a failed boot is not. If the app won't boot, fix it from a terminal — mbeditor can't help until the app can start.
Turning it off. Set config.resilient_routing = false as an escape hatch (e.g. to debug a middleware-ordering conflict). With the flag off, no mbeditor middleware is inserted and the private route set is never built; mbeditor is then served only through the normal mount and shares the fate of a broken config/routes.rb.
Keyboard Shortcuts
| Shortcut | Action |
|---|---|
Ctrl+P |
Quick-open file by name |
Ctrl+S |
Save the active file |
Ctrl+Shift+S |
Save all dirty files |
Alt+Shift+F |
Format the active file |
Ctrl+Shift+G |
Toggle the git panel |
Ctrl+Shift+T |
Run only the test under the cursor |
Ctrl+Z / Ctrl+Y |
Undo / Redo (Monaco built-in) |
Host Requirements (Optional)
The gem keeps host/tooling responsibilities in the host app:
rubocopandrubocop-railsgems (optional, required for Ruby lint/format endpoints)haml_lintgem (optional, required for HAML lint — add to your app's Gemfile if needed)gitinstalled in environment (for Git panel data)minitestorrspecin the host app's bundle (required for the test runner)actioncableframework/gem (optional, required only for realtime file-change push + websocket state saves)ruby-lspgem (optional — see below)
All lint and test tools are auto-detected at runtime. The engine gracefully disables features if the tools are not available. Neither rubocop, haml_lint, nor any test framework are runtime dependencies of the gem itself — they are discovered from the host app's environment.
Ruby language server (Optional)
Add ruby-lsp to the host app's development group and mbeditor uses it automatically for Ruby go-to-definition, hover, completion, and diagnostics:
gem "ruby-lsp", require: false, group: :development
gem "ruby-lsp-rails", require: false, group: :development # Rails-aware results
ruby-lsp-rails needs no mbeditor configuration — ruby-lsp loads it as an
addon, so associations, model attributes, and route helpers start resolving on
their own.
What changes when it's present:
- Diagnostics. Ruby files are checked by ruby-lsp instead of booting RuboCop over HTTP on every debounce, so you also get Prism syntax errors and warnings alongside RuboCop offenses. Quick-fix lightbulbs still work for correctable cops. Very large files fall back to syntax-only diagnostics.
- Definitions, hover, completion. Answered from the language server's index rather than a workspace grep, including your unsaved buffer contents.
Everything degrades on its own: if ruby-lsp is missing, times out (its first index of a large app takes a while), or crashes, that request falls back to the built-in grep/Ripper services. ERB templates always use the built-in services — ruby-lsp cannot parse ERB.
Realtime via Action Cable (Optional)
Mbeditor works without Action Cable. If Action Cable is unavailable, unreachable, or returns transient errors, the editor automatically falls back to polling.
To enable realtime features in a host app:
- Ensure Action Cable is enabled in the host app (for apps that do not load it by default, add the framework/gem explicitly).
- Mount cable in host routes:
mount ActionCable.server => '/cable'
- Make Action Cable JavaScript available to the page (for asset-pipeline apps,
actioncable.jsis typically sufficient).
If any of these are missing, mbeditor still runs in polling mode.
Syntax Highlighting Support
Monaco runtime assets are served from the engine route namespace (/mbeditor/monaco-editor/* and /mbeditor/monaco_worker.js).
The gem includes syntax highlighting for common Rails and React development file types:
Web & Template Languages:
- Ruby (.rb, Gemfile, gemspec, Rakefile)
- HTML
- ERB (.html.erb, .erb) — dedicated ERB grammar, plus Ruby intellisense
inside
<% %>tags: hover, completion, go-to-definition (Ctrl/Cmd+click or F12) and auto-end, all inert in the surrounding HTML. ERB uses the built-in workspace services rather than ruby-lsp, which cannot parse ERB. - HAML (.haml) — plaintext syntax highlighting (no dedicated HAML grammar in Monaco; haml-lint provides inline error markers when available)
- CSS and SCSS stylesheets
JavaScript & React:
- JavaScript / JSX (.js, .jsx, .js.jsx) — Monaco's TypeScript worker runs in
checked-JS mode with JSX enabled. Built for the Sprockets world where every
top-level
var/function/class(andwindow.X =assignment) is a global: the editor scans the workspace once at boot (GET /js_globals) and declares all of them as ambient globals, so cross-file component references need noimportand produce no "Cannot find name" diagnostics. The list refreshes automatically when files change. Runtime-only globals the static scan can't see (e.g.Routes,I18n) can be declared viaconfig.js_global_identifiers = %w[Routes I18n]. Known limits: everything is typedany(no cross-file type inference), and genuinely undefined names are shown as warnings, not errors. - TypeScript (.ts, .tsx)
Configuration & Documentation:
- YAML (.yml, .yaml)
- Markdown (.md)
These language modules are packaged locally with the gem for true offline operation. No network fallback is needed—all highlighting works without internet connectivity.
Asset Pipeline
Mbeditor requires Sprockets (sprockets-rails >= 3.4). Host apps using Propshaft as their asset pipeline are not supported — the engine depends on Sprockets directives to load its JavaScript and CSS assets.
Development
A minimal dummy Rails app is included for local development and testing:
cd test/dummy && rails server
Then visit http://localhost:3000/mbeditor.
Testing
The test suite uses Minitest via the dummy Rails app. Run all tests from the project root:
bundle exec rake test
Run a single test file:
bundle exec ruby -Itest test/controllers/mbeditor/editors_controller_test.rb
Run a single test by name:
bundle exec ruby -Itest test/controllers/mbeditor/editors_controller_test.rb -n test_ping_returns_ok