mbeditor 0.10.0 → 0.10.1
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 +37 -0
- data/README.md +0 -106
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +39 -1
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +8 -4
- data/lib/mbeditor/configuration.rb +1 -2
- data/lib/mbeditor/engine.rb +0 -3
- data/lib/mbeditor/version.rb +1 -1
- metadata +1 -2
- data/lib/mbeditor/file_watcher.rb +0 -136
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 47a8199a082957d396dce3cfa9de010ece11a11b2b938cf0cf81adfa6dd171f1
|
|
4
|
+
data.tar.gz: 30dfee949bc6d06245c6bd1ba5b682431b36b064e0ad510b0dac44b320c5a287
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 81c342ce0dc3f80c44259ae3a130710013dcb81f45052d4cc4488d2a7dc112f7decc6b5d67f3f6aaa81cbcd7ef391d82136ee1adccdd369b369c7cfcd4d6afa7
|
|
7
|
+
data.tar.gz: b201dda9b9fe55e9f44981725788b1587bc052f940652e8eb7d86a8dcc3d17c45bb0984f71cd3bbbd9f3a001b3b0125ac09a9e53eedd3e580eab96a445be6a17
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,43 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.10.1] - 2026-07-27
|
|
9
|
+
|
|
10
|
+
### Removed
|
|
11
|
+
- **The `listen`-based file watcher, and with it `config.watch_files`.**
|
|
12
|
+
0.10.0 enabled a workspace watcher by default. On Linux each watched
|
|
13
|
+
directory costs an inotify watch from `fs.inotify.max_user_watches`, a
|
|
14
|
+
*per-user* budget shared with everything else watching files — including the
|
|
15
|
+
host app's own code reloader and any other gem using `listen`. Exhausting it
|
|
16
|
+
raises `iNotify max watches exceeded`, and because `listen` reports some of
|
|
17
|
+
those failures from its own background thread, mbeditor could not even
|
|
18
|
+
rescue them. Claiming a share of a scarce OS resource by default was the
|
|
19
|
+
wrong trade for a development tool, and raising the limit needs root, which
|
|
20
|
+
a developer may not have.
|
|
21
|
+
|
|
22
|
+
Nothing is lost: external changes are picked up by polling, which is how the
|
|
23
|
+
editor already tracked git state. If you set `config.watch_files`, remove it
|
|
24
|
+
— it is now ignored.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- **The file tree never refreshed for changes made outside the editor.** Its
|
|
28
|
+
10-second poll returned early whenever the Action Cable socket was connected,
|
|
29
|
+
on the reasoning that the push covered it — but the server only broadcasts
|
|
30
|
+
from mbeditor's own mutation endpoints. With a socket connected, which is the
|
|
31
|
+
normal case, an external `git checkout` or generator run was never picked up.
|
|
32
|
+
The poll now always runs; the push remains the instant path for our own
|
|
33
|
+
writes. This is the bug the 0.10.0 watcher was compensating for.
|
|
34
|
+
- **Git line-number tinting went stale for external changes**, for the same
|
|
35
|
+
reason, and its refresh timer was being cleared and recreated on every
|
|
36
|
+
re-render so it never survived long enough to fire. It now polls on its own
|
|
37
|
+
timer and on window focus, matching the file tree.
|
|
38
|
+
- **Watched paths were dropped when the workspace was reached through a
|
|
39
|
+
symlink** (macOS `/var` → `/private/var`, or a symlinked checkout), because
|
|
40
|
+
reported paths resolve to the real path and no longer matched the configured
|
|
41
|
+
root.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
8
45
|
## [0.10.0] - 2026-07-27
|
|
9
46
|
|
|
10
47
|
### Added
|
data/README.md
CHANGED
|
@@ -16,7 +16,6 @@ Mbeditor (Mini Browser Editor) is a mountable Rails engine that adds a browser-b
|
|
|
16
16
|
- Optional RuboCop lint and format endpoints (uses host app RuboCop)
|
|
17
17
|
- Optional Ruby language-server integration (definitions, hover, completion, diagnostics)
|
|
18
18
|
- Optional test runner with inline failure markers and a dedicated results panel (Minitest and RSpec)
|
|
19
|
-
- Optional workspace file watching, so changes made outside the editor refresh the tree and git decorations
|
|
20
19
|
|
|
21
20
|
## Security Warning
|
|
22
21
|
Mbeditor exposes read and write access to your Rails application directory over HTTP. It is intended only for local development.
|
|
@@ -101,7 +100,6 @@ end
|
|
|
101
100
|
| `ruby_lsp` | `:auto` | Use the host's [ruby-lsp](https://github.com/Shopify/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. |
|
|
102
101
|
| `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`. |
|
|
103
102
|
| `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. |
|
|
104
|
-
| `watch_files` | `:auto` | Watch the workspace for changes made outside the editor (a terminal `git checkout`, a generator, another editor) and push a refresh to open clients. Requires the host's [`listen`](https://github.com/guard/listen) gem; without it the editor behaves as before and only announces its own writes. `false` disables. |
|
|
105
103
|
|
|
106
104
|
### Authentication
|
|
107
105
|
|
|
@@ -205,113 +203,9 @@ The gem keeps host/tooling responsibilities in the host app:
|
|
|
205
203
|
- `minitest` or `rspec` in the host app's bundle (required for the test runner)
|
|
206
204
|
- `actioncable` framework/gem (optional, required only for realtime file-change push + websocket state saves)
|
|
207
205
|
- `ruby-lsp` gem (optional — see below)
|
|
208
|
-
- `listen` gem (optional — see below)
|
|
209
206
|
|
|
210
207
|
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.
|
|
211
208
|
|
|
212
|
-
### Workspace file watching (Optional)
|
|
213
|
-
|
|
214
|
-
Add [`listen`](https://github.com/guard/listen) to the host app's development
|
|
215
|
-
group and mbeditor watches the workspace for changes it did not make itself:
|
|
216
|
-
|
|
217
|
-
```ruby
|
|
218
|
-
group :development do
|
|
219
|
-
gem 'listen'
|
|
220
|
-
end
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
Without it, only writes made *through* the editor announce themselves, so a
|
|
224
|
-
`git checkout` or a generator run in a terminal leaves the file tree and the
|
|
225
|
-
git line-number colours stale until you reopen the file. With it, those refresh
|
|
226
|
-
on their own.
|
|
227
|
-
|
|
228
|
-
The watcher runs only in `allowed_environments` and only in processes that
|
|
229
|
-
serve requests, so rake tasks and consoles never start one. It respects
|
|
230
|
-
`excluded_paths`, coalesces bursts (a branch switch is one refresh, not
|
|
231
|
-
hundreds), and requires Action Cable to actually deliver the push. If it cannot
|
|
232
|
-
start — inotify limits, permissions — it logs a warning and the editor carries
|
|
233
|
-
on exactly as it would without the gem. Set `config.watch_files = false` to
|
|
234
|
-
disable it even when `listen` is present.
|
|
235
|
-
|
|
236
|
-
### Ruby language server (Optional)
|
|
237
|
-
|
|
238
|
-
Add [ruby-lsp](https://github.com/Shopify/ruby-lsp) to the host app's
|
|
239
|
-
development group and mbeditor uses it automatically for Ruby
|
|
240
|
-
go-to-definition, hover, completion, and diagnostics:
|
|
241
|
-
|
|
242
|
-
```ruby
|
|
243
|
-
gem "ruby-lsp", require: false, group: :development
|
|
244
|
-
gem "ruby-lsp-rails", require: false, group: :development # Rails-aware results
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
`ruby-lsp-rails` needs no mbeditor configuration — ruby-lsp loads it as an
|
|
248
|
-
addon, so associations, model attributes, and route helpers start resolving on
|
|
249
|
-
their own.
|
|
250
|
-
|
|
251
|
-
What changes when it's present:
|
|
252
|
-
|
|
253
|
-
- **Diagnostics.** Ruby files are checked by ruby-lsp instead of booting
|
|
254
|
-
RuboCop over HTTP on every debounce, so you also get Prism syntax errors and
|
|
255
|
-
warnings alongside RuboCop offenses. Quick-fix lightbulbs still work for
|
|
256
|
-
correctable cops. Very large files fall back to syntax-only diagnostics.
|
|
257
|
-
- **Definitions, hover, completion.** Answered from the language server's index
|
|
258
|
-
rather than a workspace grep, including your unsaved buffer contents.
|
|
259
|
-
|
|
260
|
-
Everything degrades on its own: if ruby-lsp is missing, times out (its first
|
|
261
|
-
index of a large app takes a while), or crashes, that request falls back to the
|
|
262
|
-
built-in grep/Ripper services. ERB templates always use the built-in services —
|
|
263
|
-
ruby-lsp cannot parse ERB.
|
|
264
|
-
|
|
265
|
-
### Realtime via Action Cable (Optional)
|
|
266
|
-
|
|
267
|
-
Mbeditor works without Action Cable. If Action Cable is unavailable, unreachable, or returns transient errors, the editor automatically falls back to polling.
|
|
268
|
-
|
|
269
|
-
To enable realtime features in a host app:
|
|
270
|
-
|
|
271
|
-
1. Ensure Action Cable is enabled in the host app (for apps that do not load it by default, add the framework/gem explicitly).
|
|
272
|
-
2. Mount cable in host routes:
|
|
273
|
-
|
|
274
|
-
```ruby
|
|
275
|
-
mount ActionCable.server => '/cable'
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
3. Make Action Cable JavaScript available to the page (for asset-pipeline apps, `actioncable.js` is typically sufficient).
|
|
279
|
-
|
|
280
|
-
If any of these are missing, mbeditor still runs in polling mode.
|
|
281
|
-
|
|
282
|
-
### Syntax Highlighting Support
|
|
283
|
-
Monaco runtime assets are served from the engine route namespace (`/mbeditor/monaco-editor/*` and `/mbeditor/monaco_worker.js`).
|
|
284
|
-
The gem includes syntax highlighting for common Rails and React development file types:
|
|
285
|
-
|
|
286
|
-
**Web & Template Languages:**
|
|
287
|
-
- **Ruby** (.rb, Gemfile, gemspec, Rakefile)
|
|
288
|
-
- **HTML**
|
|
289
|
-
- **ERB** (.html.erb, .erb) — dedicated ERB grammar, plus Ruby intellisense
|
|
290
|
-
inside `<% %>` tags: hover, completion, go-to-definition (Ctrl/Cmd+click or
|
|
291
|
-
F12) and auto-`end`, all inert in the surrounding HTML. ERB uses the built-in
|
|
292
|
-
workspace services rather than ruby-lsp, which cannot parse ERB.
|
|
293
|
-
- **HAML** (.haml) — plaintext syntax highlighting (no dedicated HAML grammar in Monaco; haml-lint provides inline error markers when available)
|
|
294
|
-
- **CSS** and **SCSS** stylesheets
|
|
295
|
-
|
|
296
|
-
**JavaScript & React:**
|
|
297
|
-
- **JavaScript / JSX** (.js, .jsx, .js.jsx) — Monaco's TypeScript worker runs in
|
|
298
|
-
checked-JS mode with JSX enabled. Built for the Sprockets world where every
|
|
299
|
-
top-level `var`/`function`/`class` (and `window.X =` assignment) is a global:
|
|
300
|
-
the editor scans the workspace once at boot (`GET /js_globals`) and declares
|
|
301
|
-
all of them as ambient globals, so cross-file component references need no
|
|
302
|
-
`import` and produce no "Cannot find name" diagnostics. The list refreshes
|
|
303
|
-
automatically when files change. Runtime-only globals the static scan can't
|
|
304
|
-
see (e.g. `Routes`, `I18n`) can be declared via
|
|
305
|
-
`config.js_global_identifiers = %w[Routes I18n]`.
|
|
306
|
-
Known limits: everything is typed `any` (no cross-file type inference), and
|
|
307
|
-
genuinely undefined names are shown as warnings, not errors.
|
|
308
|
-
- **TypeScript** (.ts, .tsx)
|
|
309
|
-
|
|
310
|
-
**Configuration & Documentation:**
|
|
311
|
-
- **YAML** (.yml, .yaml)
|
|
312
|
-
- **Markdown** (.md)
|
|
313
|
-
|
|
314
|
-
These language modules are packaged locally with the gem for true offline operation. No network fallback is needed—all highlighting works without internet connectivity.
|
|
315
209
|
|
|
316
210
|
## Asset Pipeline
|
|
317
211
|
|
|
@@ -70,6 +70,9 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
70
70
|
|
|
71
71
|
var blameDecorationsRef = useRef([]);
|
|
72
72
|
var gitLineDecorationsRef = useRef([]);
|
|
73
|
+
// Latest git line-diff refresh, read by the poll effect so its interval does
|
|
74
|
+
// not have to be torn down whenever the active tab changes.
|
|
75
|
+
var gitLineRefreshRef = useRef(null);
|
|
73
76
|
var blameZoneIdsRef = useRef([]);
|
|
74
77
|
var testDecorationIdsRef = useRef([]);
|
|
75
78
|
var testZoneIdsRef = useRef([]);
|
|
@@ -1290,7 +1293,14 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1290
1293
|
// The ranges come from `git diff -U0` against HEAD, so they describe the file
|
|
1291
1294
|
// as last written to disk. Monaco anchors decorations to the model and shifts
|
|
1292
1295
|
// them as you type, which keeps them roughly right mid-edit; the authoritative
|
|
1293
|
-
// refresh happens
|
|
1296
|
+
// refresh happens on the signals wired up at the end of this effect.
|
|
1297
|
+
|
|
1298
|
+
// Poll cadence for the tint, matching the file tree's. Slower to react than a
|
|
1299
|
+
// filesystem watcher would be, at the cost of one `git diff -U0` on one file
|
|
1300
|
+
// per visible pane — against a resource nothing else is competing for, rather
|
|
1301
|
+
// than a share of the kernel's inotify budget.
|
|
1302
|
+
var GIT_LINE_POLL_MS = 10000;
|
|
1303
|
+
|
|
1294
1304
|
useEffect(function () {
|
|
1295
1305
|
if (!gitAvailable || !tab.path || tab.isDiff || tab.isCombinedDiff) return;
|
|
1296
1306
|
|
|
@@ -1347,17 +1357,45 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1347
1357
|
|
|
1348
1358
|
refresh();
|
|
1349
1359
|
|
|
1360
|
+
// Publish the current refresh for the poll below, which lives in its own
|
|
1361
|
+
// effect so a re-render of this one cannot restart its clock.
|
|
1362
|
+
gitLineRefreshRef.current = refresh;
|
|
1363
|
+
|
|
1364
|
+
// Immediate path for writes mbeditor made itself.
|
|
1350
1365
|
var hasSocket = typeof WebSocketService !== 'undefined' && WebSocketService.onFilesChanged;
|
|
1351
1366
|
var onChanged = hasSocket ? function () { refresh(); } : null;
|
|
1352
1367
|
if (onChanged) WebSocketService.onFilesChanged(onChanged);
|
|
1353
1368
|
|
|
1354
1369
|
return function () {
|
|
1355
1370
|
cancelled = true;
|
|
1371
|
+
gitLineRefreshRef.current = null;
|
|
1356
1372
|
if (onChanged && WebSocketService.offFilesChanged) WebSocketService.offFilesChanged(onChanged);
|
|
1357
1373
|
clear();
|
|
1358
1374
|
};
|
|
1359
1375
|
}, [tab.path, tab.externalContentVersion, tab.isDiff, tab.isCombinedDiff, gitAvailable]);
|
|
1360
1376
|
|
|
1377
|
+
// The poll for changes made outside the editor — a terminal commit or branch
|
|
1378
|
+
// switch alters the diff without touching this buffer.
|
|
1379
|
+
//
|
|
1380
|
+
// Deliberately its own effect with no dependencies. Held inside the effect
|
|
1381
|
+
// above, the interval was cleared and recreated on every re-render of that
|
|
1382
|
+
// one and never survived long enough to fire, so the tint only ever updated
|
|
1383
|
+
// via the WebSocket — i.e. never for external changes, which is the whole
|
|
1384
|
+
// point of it. Reading the refresh through a ref keeps this clock running
|
|
1385
|
+
// across tab switches.
|
|
1386
|
+
useEffect(function () {
|
|
1387
|
+
var tick = function () {
|
|
1388
|
+
if (document.hidden) return;
|
|
1389
|
+
if (gitLineRefreshRef.current) gitLineRefreshRef.current();
|
|
1390
|
+
};
|
|
1391
|
+
var intervalId = setInterval(tick, GIT_LINE_POLL_MS);
|
|
1392
|
+
window.addEventListener('focus', tick);
|
|
1393
|
+
return function () {
|
|
1394
|
+
clearInterval(intervalId);
|
|
1395
|
+
window.removeEventListener('focus', tick);
|
|
1396
|
+
};
|
|
1397
|
+
}, []);
|
|
1398
|
+
|
|
1361
1399
|
// Handle Blame data fetching
|
|
1362
1400
|
useEffect(function () {
|
|
1363
1401
|
if (!isBlameVisible) {
|
|
@@ -1498,14 +1498,18 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1498
1498
|
});
|
|
1499
1499
|
}
|
|
1500
1500
|
|
|
1501
|
-
// Auto-refresh the file tree every 10s to pick up external changes (new files,
|
|
1502
|
-
//
|
|
1503
|
-
//
|
|
1501
|
+
// Auto-refresh the file tree every 10s to pick up external changes (new files,
|
|
1502
|
+
// deletions, a branch switch in a terminal).
|
|
1503
|
+
//
|
|
1504
|
+
// This runs whether or not the WebSocket is connected. It used to skip when
|
|
1505
|
+
// connected, on the reasoning that the push covered it — but the server only
|
|
1506
|
+
// broadcasts from mbeditor's own mutation endpoints, so a connected socket
|
|
1507
|
+
// meant external changes were never picked up at all. The push remains the
|
|
1508
|
+
// instant path for our own writes; this is what catches everything else.
|
|
1504
1509
|
// Uses functional setTreeData to skip the re-render when nothing has changed.
|
|
1505
1510
|
useEffect(function () {
|
|
1506
1511
|
var intervalId = setInterval(function () {
|
|
1507
1512
|
if (document.hidden) return;
|
|
1508
|
-
if (WebSocketService.isConnected()) return; // WebSocket is handling refreshes
|
|
1509
1513
|
FileService.getTree().then(function (data) {
|
|
1510
1514
|
var newData = data || [];
|
|
1511
1515
|
setTreeData(function (prevData) {
|
|
@@ -11,7 +11,7 @@ module Mbeditor
|
|
|
11
11
|
:mount_path, :resilient_routing, :js_global_identifiers,
|
|
12
12
|
:js_syntax_check, :babel_standalone_path,
|
|
13
13
|
:ruby_lsp, :ruby_lsp_command, :ruby_lsp_timeout,
|
|
14
|
-
:search_respect_gitignore
|
|
14
|
+
:search_respect_gitignore
|
|
15
15
|
|
|
16
16
|
def initialize
|
|
17
17
|
@allowed_environments = [:development]
|
|
@@ -42,7 +42,6 @@ module Mbeditor
|
|
|
42
42
|
@ruby_lsp_timeout = 3 # seconds per LSP request before falling back to the built-in services
|
|
43
43
|
@mount_path = nil # explicit URL prefix override; nil falls through to detection/"/mbeditor"
|
|
44
44
|
@resilient_routing = true # serve /mbeditor from middleware so the editor survives a broken host routes.rb; false is the escape hatch
|
|
45
|
-
@watch_files = :auto # watch the workspace for changes made outside the editor when the host has the `listen` gem; false disables
|
|
46
45
|
end
|
|
47
46
|
end
|
|
48
47
|
end
|
data/lib/mbeditor/engine.rb
CHANGED
|
@@ -4,7 +4,6 @@ require "mbeditor/rack/silence_ping_request"
|
|
|
4
4
|
require "mbeditor/rack/handle_pending_migrations"
|
|
5
5
|
require "mbeditor/rack/resilient_router"
|
|
6
6
|
require "mbeditor/cable_log_filter"
|
|
7
|
-
require "mbeditor/file_watcher"
|
|
8
7
|
|
|
9
8
|
module Mbeditor
|
|
10
9
|
class Engine < ::Rails::Engine
|
|
@@ -82,8 +81,6 @@ module Mbeditor
|
|
|
82
81
|
raise ArgumentError, "[mbeditor] config.workspace_root is set to '#{cfg.workspace_root}' but that path is not a directory"
|
|
83
82
|
end
|
|
84
83
|
|
|
85
|
-
Mbeditor::FileWatcher.start_if_enabled
|
|
86
|
-
|
|
87
84
|
if cfg.redmine_enabled
|
|
88
85
|
require "uri"
|
|
89
86
|
if cfg.redmine_url.blank?
|
data/lib/mbeditor/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mbeditor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.10.
|
|
4
|
+
version: 0.10.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oliver Noonan
|
|
@@ -136,7 +136,6 @@ files:
|
|
|
136
136
|
- lib/mbeditor/configuration.rb
|
|
137
137
|
- lib/mbeditor/editor_bootstrap.rb
|
|
138
138
|
- lib/mbeditor/engine.rb
|
|
139
|
-
- lib/mbeditor/file_watcher.rb
|
|
140
139
|
- lib/mbeditor/mount_path.rb
|
|
141
140
|
- lib/mbeditor/private_routes.rb
|
|
142
141
|
- lib/mbeditor/rack/handle_pending_migrations.rb
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Mbeditor
|
|
4
|
-
# Watches the workspace for changes made outside the editor — a terminal
|
|
5
|
-
# `git checkout`, a rebase, another editor, a generator — and broadcasts the
|
|
6
|
-
# same `files_changed` payload the mutation endpoints send. Clients refresh
|
|
7
|
-
# the file tree, git line-number tinting and cached globals from it.
|
|
8
|
-
#
|
|
9
|
-
# The `listen` gem is an optional host dependency. Without it the editor
|
|
10
|
-
# behaves exactly as before: only changes made *through* mbeditor announce
|
|
11
|
-
# themselves. Nothing warns loudly about the missing gem — it is opt-in.
|
|
12
|
-
#
|
|
13
|
-
# Only one watcher runs per process. It is deliberately not started in test
|
|
14
|
-
# or in non-server processes (rake, console, the Rails runner), where a
|
|
15
|
-
# background listener thread is pure overhead.
|
|
16
|
-
module FileWatcher
|
|
17
|
-
# Coalesce bursts: a branch switch touches hundreds of files, and each one
|
|
18
|
-
# would otherwise be its own broadcast.
|
|
19
|
-
DEBOUNCE_SECONDS = 0.3
|
|
20
|
-
|
|
21
|
-
class << self
|
|
22
|
-
def available?
|
|
23
|
-
return @available if defined?(@available)
|
|
24
|
-
|
|
25
|
-
@available = begin
|
|
26
|
-
require "listen"
|
|
27
|
-
true
|
|
28
|
-
rescue LoadError
|
|
29
|
-
false
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def running?
|
|
34
|
-
!@listener.nil?
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Boot entry point. Confined to the environments the editor is allowed in
|
|
38
|
-
# and to processes that actually serve requests — a rake task or console
|
|
39
|
-
# has no client to broadcast to, and a listener thread there would only
|
|
40
|
-
# burn file handles. MBEDITOR_FORCE_WATCH overrides the process check for
|
|
41
|
-
# unusual servers and for tests.
|
|
42
|
-
def start_if_enabled
|
|
43
|
-
cfg = Mbeditor.configuration
|
|
44
|
-
return false if cfg.watch_files == false
|
|
45
|
-
return false unless cfg.allowed_environments.map(&:to_s).include?(Rails.env.to_s)
|
|
46
|
-
return false unless serving_requests?
|
|
47
|
-
|
|
48
|
-
start(cfg.workspace_root.presence || Rails.root.to_s)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# Returns true when a watcher was started, false for every reason not to
|
|
52
|
-
# (gem absent, already running, no workspace, disabled by config).
|
|
53
|
-
def start(root)
|
|
54
|
-
return false unless available?
|
|
55
|
-
return false if running?
|
|
56
|
-
|
|
57
|
-
root = root.to_s
|
|
58
|
-
return false if root.empty? || !File.directory?(root)
|
|
59
|
-
|
|
60
|
-
ignores = ignore_patterns(root)
|
|
61
|
-
@listener = ::Listen.to(root, ignore: ignores, latency: DEBOUNCE_SECONDS) do |modified, added, removed|
|
|
62
|
-
broadcast(root, modified + added + removed)
|
|
63
|
-
end
|
|
64
|
-
@listener.start
|
|
65
|
-
Rails.logger.info("[mbeditor] watching #{root} for external changes")
|
|
66
|
-
true
|
|
67
|
-
rescue StandardError => e
|
|
68
|
-
# A watcher that cannot start must never take the host app down with it:
|
|
69
|
-
# inotify limits on Linux, permission issues, an unreadable root.
|
|
70
|
-
Rails.logger.warn("[mbeditor] file watcher failed to start: #{e.class}: #{e.message}")
|
|
71
|
-
@listener = nil
|
|
72
|
-
false
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def stop
|
|
76
|
-
@listener&.stop
|
|
77
|
-
rescue StandardError
|
|
78
|
-
nil
|
|
79
|
-
ensure
|
|
80
|
-
@listener = nil
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
private
|
|
84
|
-
|
|
85
|
-
def serving_requests?
|
|
86
|
-
return true if ENV["MBEDITOR_FORCE_WATCH"]
|
|
87
|
-
|
|
88
|
-
defined?(Rails::Server) || defined?(Puma::Server) || defined?(Unicorn) || defined?(Passenger)
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# `listen` matches ignores against paths relative to the watched root, so
|
|
92
|
-
# the configured exclusions become anchored regexps. Escaping matters:
|
|
93
|
-
# entries like "vendor/bundle" and "public/assets" contain separators, and
|
|
94
|
-
# a stray metacharacter in host config should not build a bogus pattern.
|
|
95
|
-
def ignore_patterns(root)
|
|
96
|
-
Array(Mbeditor.configuration.excluded_paths).map(&:to_s).reject(&:empty?).map do |path|
|
|
97
|
-
%r{\A#{Regexp.escape(path.delete_prefix("/").delete_suffix("/"))}(/|\z)}
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# Paths arrive absolute. Anything that does not sit under the workspace
|
|
102
|
-
# is dropped rather than sent raw: the client keys everything by
|
|
103
|
-
# workspace-relative path, and an absolute one would leak host layout.
|
|
104
|
-
def relative_paths(root, paths)
|
|
105
|
-
paths.filter_map do |path|
|
|
106
|
-
rel = path.to_s.delete_prefix("#{root}/")
|
|
107
|
-
rel unless rel.empty? || rel == path.to_s
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def broadcast(root, paths)
|
|
112
|
-
relative = relative_paths(root, paths)
|
|
113
|
-
|
|
114
|
-
invalidate_caches(root)
|
|
115
|
-
return unless defined?(ActionCable.server)
|
|
116
|
-
|
|
117
|
-
payload = { type: "files_changed" }
|
|
118
|
-
payload[:paths] = relative.first(200) if relative.any?
|
|
119
|
-
ActionCable.server.broadcast("mbeditor_editor", payload)
|
|
120
|
-
rescue StandardError => e
|
|
121
|
-
Rails.logger.warn("[mbeditor] file watcher broadcast failed: #{e.class}: #{e.message}")
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
# Mirrors EditorsController#broadcast_files_changed: a change the editor
|
|
125
|
-
# did not make invalidates exactly the same caches as one it did.
|
|
126
|
-
def invalidate_caches(root)
|
|
127
|
-
FileTreeService.invalidate(root)
|
|
128
|
-
SearchReplaceService.invalidate_cache(root)
|
|
129
|
-
JsGlobalsService.invalidate(root)
|
|
130
|
-
GitInfoService.invalidate(root)
|
|
131
|
-
rescue StandardError => e
|
|
132
|
-
Rails.logger.warn("[mbeditor] file watcher cache invalidation failed: #{e.class}: #{e.message}")
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
end
|