mbeditor 0.4.2 → 0.4.3
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 +13 -0
- data/README.md +18 -0
- data/app/assets/javascripts/mbeditor/websocket_service.js +96 -27
- data/app/assets/stylesheets/mbeditor/application.css +19 -0
- data/app/channels/mbeditor/editor_channel.rb +4 -2
- data/app/controllers/mbeditor/editors_controller.rb +18 -1
- data/lib/mbeditor/cable_log_filter.rb +30 -2
- data/lib/mbeditor/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cf6000382b991d7d35aa850c38fc80c743089c60f4a2277310f552e5a3b85477
|
|
4
|
+
data.tar.gz: 4d9c42bc71a9db592172eb09a601b134628f3c7f4d619a271349dda9b675c5ac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9e4b14f3d506a23c1f157a7dd91d4c522a568623fd555b3ef10e6e83e561b4bdcaf040b822ea1c2ec86ca1eed8f47992f7fa1060cb18a9600ee2f43b10de4730
|
|
7
|
+
data.tar.gz: '0911b0a22050d16d96a1e841ae16e73673fd0579549efd5590d538efe405ca66e7f1470597680e0102954b4d4ce6ef9337d02a22dc736c487998f990808ff34f'
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ 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.4.3] - 2026-04-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Documented optional Action Cable host-app setup and fallback behavior for realtime editor updates.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- `bundle exec rake test` now includes `test/lib/**/*_test.rb` so release validation covers library-level regression tests.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- Action Cable availability detection now respects whether `/cable` is actually mounted, and websocket handshake failures fall back to polling without noisy console errors.
|
|
18
|
+
- Circular loading indicators keep animating even when global reduced-motion styles are active in production.
|
|
19
|
+
- `CableLogFilter` now remains compatible with untagged logger stacks used outside full Action Cable setups.
|
|
20
|
+
|
|
8
21
|
## [0.4.2] - 2026-04-22
|
|
9
22
|
|
|
10
23
|
### Fixed
|
data/README.md
CHANGED
|
@@ -121,9 +121,27 @@ The gem keeps host/tooling responsibilities in the host app:
|
|
|
121
121
|
- `haml_lint` gem (optional, required for HAML lint — add to your app's Gemfile if needed)
|
|
122
122
|
- `git` installed in environment (for Git panel data)
|
|
123
123
|
- `minitest` or `rspec` in the host app's bundle (required for the test runner)
|
|
124
|
+
- `actioncable` framework/gem (optional, required only for realtime file-change push + websocket state saves)
|
|
124
125
|
|
|
125
126
|
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.
|
|
126
127
|
|
|
128
|
+
### Realtime via Action Cable (Optional)
|
|
129
|
+
|
|
130
|
+
Mbeditor works without Action Cable. If Action Cable is unavailable, unreachable, or returns transient errors, the editor automatically falls back to polling.
|
|
131
|
+
|
|
132
|
+
To enable realtime features in a host app:
|
|
133
|
+
|
|
134
|
+
1. Ensure Action Cable is enabled in the host app (for apps that do not load it by default, add the framework/gem explicitly).
|
|
135
|
+
2. Mount cable in host routes:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
mount ActionCable.server => '/cable'
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
3. Make Action Cable JavaScript available to the page (for asset-pipeline apps, `actioncable.js` is typically sufficient).
|
|
142
|
+
|
|
143
|
+
If any of these are missing, mbeditor still runs in polling mode.
|
|
144
|
+
|
|
127
145
|
### Syntax Highlighting Support
|
|
128
146
|
Monaco runtime assets are served from the engine route namespace (`/mbeditor/monaco-editor/*` and `/mbeditor/monaco_worker.js`).
|
|
129
147
|
The gem includes syntax highlighting for common Rails and React development file types:
|
|
@@ -11,6 +11,10 @@ var WebSocketService = (function () {
|
|
|
11
11
|
var _subscription = null;
|
|
12
12
|
var _connected = false;
|
|
13
13
|
var _filesChangedCallbacks = [];
|
|
14
|
+
var _serverSupportsWs = false;
|
|
15
|
+
var _reconnectTimer = null;
|
|
16
|
+
var _lastCableAttemptAt = 0;
|
|
17
|
+
var RECONNECT_INTERVAL_MS = 30000;
|
|
14
18
|
|
|
15
19
|
// ---------------------------------------------------------------------------
|
|
16
20
|
// Internal helpers
|
|
@@ -20,33 +24,78 @@ var WebSocketService = (function () {
|
|
|
20
24
|
return typeof window.ActionCable !== 'undefined';
|
|
21
25
|
}
|
|
22
26
|
|
|
27
|
+
function _cableUrl() {
|
|
28
|
+
return window.MBEDITOR_CABLE_URL || '/cable';
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
function _getConsumer() {
|
|
24
32
|
// Reuse an existing consumer the host app may have already created (App.cable
|
|
25
33
|
// is the Rails default). Fall back to creating our own.
|
|
26
34
|
if (typeof window.App !== 'undefined' && window.App.cable) {
|
|
27
35
|
return window.App.cable;
|
|
28
36
|
}
|
|
29
|
-
|
|
30
|
-
return window.ActionCable.createConsumer(cableUrl);
|
|
37
|
+
return window.ActionCable.createConsumer(_cableUrl());
|
|
31
38
|
}
|
|
32
39
|
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
try {
|
|
40
|
+
function _cleanupConsumer() {
|
|
41
|
+
if (_subscription) {
|
|
42
|
+
try { _subscription.unsubscribe(); } catch (e) { /* ignore */ }
|
|
43
|
+
_subscription = null;
|
|
44
|
+
}
|
|
45
|
+
if (_consumer && typeof _consumer.disconnect === 'function') {
|
|
46
|
+
try { _consumer.disconnect(); } catch (e) { /* ignore */ }
|
|
47
|
+
}
|
|
48
|
+
_consumer = null;
|
|
49
|
+
_connected = false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function _isCableRelatedRejection(reason) {
|
|
53
|
+
var url = _cableUrl();
|
|
54
|
+
if (!reason) return false;
|
|
55
|
+
|
|
56
|
+
// Common browser shape: ErrorEvent with target/currentTarget = WebSocket.
|
|
57
|
+
var target = reason.target || reason.currentTarget;
|
|
58
|
+
if (target && typeof target.url === 'string' && target.url.indexOf(url) !== -1) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Some environments stringify this as "[object Event]"; only suppress if
|
|
63
|
+
// it happened right after a cable connection attempt.
|
|
64
|
+
var ageMs = Date.now() - _lastCableAttemptAt;
|
|
65
|
+
var reasonText = String(reason);
|
|
66
|
+
if (reasonText.indexOf('[object Event]') !== -1 && ageMs >= 0 && ageMs < 3000) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function _installUnhandledRejectionGuard() {
|
|
74
|
+
if (window.__mbeditorCableRejectionGuardInstalled) return;
|
|
75
|
+
window.__mbeditorCableRejectionGuardInstalled = true;
|
|
76
|
+
window.addEventListener('unhandledrejection', function (event) {
|
|
77
|
+
if (_isCableRelatedRejection(event.reason)) {
|
|
78
|
+
// Keep cable handshake failures internal; polling fallback remains active.
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
}
|
|
36
81
|
});
|
|
37
82
|
}
|
|
38
83
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
84
|
+
function _scheduleReconnect() {
|
|
85
|
+
if (_reconnectTimer || !_serverSupportsWs) return;
|
|
86
|
+
_reconnectTimer = setTimeout(function () {
|
|
87
|
+
_reconnectTimer = null;
|
|
88
|
+
_attemptConnect();
|
|
89
|
+
}, RECONNECT_INTERVAL_MS);
|
|
90
|
+
}
|
|
42
91
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (!serverSupportsWs || !_isActionCableAvailable()) {
|
|
47
|
-
return; // polling remains the only refresh mechanism
|
|
92
|
+
function _attemptConnect() {
|
|
93
|
+
if (!_serverSupportsWs || !_isActionCableAvailable() || _connected || _subscription) {
|
|
94
|
+
return;
|
|
48
95
|
}
|
|
49
96
|
|
|
97
|
+
_lastCableAttemptAt = Date.now();
|
|
98
|
+
|
|
50
99
|
try {
|
|
51
100
|
_consumer = _getConsumer();
|
|
52
101
|
_subscription = _consumer.subscriptions.create(
|
|
@@ -56,15 +105,12 @@ var WebSocketService = (function () {
|
|
|
56
105
|
_connected = true;
|
|
57
106
|
},
|
|
58
107
|
disconnected: function () {
|
|
59
|
-
|
|
108
|
+
_cleanupConsumer();
|
|
109
|
+
_scheduleReconnect();
|
|
60
110
|
},
|
|
61
111
|
rejected: function () {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (_subscription) {
|
|
65
|
-
_subscription.unsubscribe();
|
|
66
|
-
_subscription = null;
|
|
67
|
-
}
|
|
112
|
+
_cleanupConsumer();
|
|
113
|
+
_scheduleReconnect();
|
|
68
114
|
},
|
|
69
115
|
received: function (data) {
|
|
70
116
|
if (data && data.type === 'files_changed') {
|
|
@@ -74,18 +120,41 @@ var WebSocketService = (function () {
|
|
|
74
120
|
}
|
|
75
121
|
);
|
|
76
122
|
} catch (e) {
|
|
77
|
-
// Any setup error means we silently stay in polling-only mode.
|
|
78
|
-
|
|
79
|
-
|
|
123
|
+
// Any setup error means we silently stay in polling-only mode for now.
|
|
124
|
+
_cleanupConsumer();
|
|
125
|
+
_scheduleReconnect();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function _emitFilesChanged(data) {
|
|
130
|
+
_filesChangedCallbacks.forEach(function (fn) {
|
|
131
|
+
try { fn(data); } catch (e) { /* ignore */ }
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Public API
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
// Call once after the workspace response is received.
|
|
140
|
+
// serverSupportsWs: boolean from workspace.actionCableEnabled
|
|
141
|
+
function connect(serverSupportsWs) {
|
|
142
|
+
_serverSupportsWs = !!serverSupportsWs;
|
|
143
|
+
if (!_serverSupportsWs || !_isActionCableAvailable()) {
|
|
144
|
+
return; // polling remains the only refresh mechanism
|
|
80
145
|
}
|
|
146
|
+
_installUnhandledRejectionGuard();
|
|
147
|
+
_attemptConnect();
|
|
148
|
+
_scheduleReconnect();
|
|
81
149
|
}
|
|
82
150
|
|
|
83
151
|
function disconnect() {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
152
|
+
_serverSupportsWs = false;
|
|
153
|
+
if (_reconnectTimer) {
|
|
154
|
+
clearTimeout(_reconnectTimer);
|
|
155
|
+
_reconnectTimer = null;
|
|
87
156
|
}
|
|
88
|
-
|
|
157
|
+
_cleanupConsumer();
|
|
89
158
|
}
|
|
90
159
|
|
|
91
160
|
// Returns true only when the WebSocket is currently live.
|
|
@@ -841,4 +841,23 @@
|
|
|
841
841
|
@keyframes mbeditor-spin {
|
|
842
842
|
to { transform: rotate(360deg); }
|
|
843
843
|
}
|
|
844
|
+
|
|
845
|
+
@media (prefers-reduced-motion: reduce) {
|
|
846
|
+
.mbeditor-spinner,
|
|
847
|
+
.editor-loading-spinner,
|
|
848
|
+
.search-loading-spinner {
|
|
849
|
+
animation-duration: 0.75s !important;
|
|
850
|
+
animation-delay: 0s !important;
|
|
851
|
+
animation-iteration-count: infinite !important;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
.editor-loading-spinner {
|
|
855
|
+
animation-duration: 0.65s !important;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
.search-loading-spinner {
|
|
859
|
+
animation-duration: 0.7s !important;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
844
863
|
.cdiff-ctx { color: var(--ide-text-muted); }
|
|
@@ -4,12 +4,14 @@ require "fileutils"
|
|
|
4
4
|
require "pathname"
|
|
5
5
|
|
|
6
6
|
module Mbeditor
|
|
7
|
-
|
|
7
|
+
CableBaseClass = defined?(ActionCable::Channel::Base) ? ActionCable::Channel::Base : Object
|
|
8
|
+
|
|
9
|
+
class EditorChannel < CableBaseClass
|
|
8
10
|
STATE_MAX_BYTES = 1 * 1024 * 1024
|
|
9
11
|
SAFE_BRANCH_NAME = /\A[a-zA-Z0-9._\-\/]+\z/
|
|
10
12
|
|
|
11
13
|
def subscribed
|
|
12
|
-
stream_from "mbeditor_editor"
|
|
14
|
+
stream_from "mbeditor_editor" if respond_to?(:stream_from)
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
def unsubscribed
|
|
@@ -39,7 +39,7 @@ module Mbeditor
|
|
|
39
39
|
blameAvailable: git_blame_available?,
|
|
40
40
|
redmineEnabled: Mbeditor.configuration.redmine_enabled == true,
|
|
41
41
|
testAvailable: test_available?,
|
|
42
|
-
actionCableEnabled:
|
|
42
|
+
actionCableEnabled: action_cable_enabled?
|
|
43
43
|
}
|
|
44
44
|
end
|
|
45
45
|
|
|
@@ -691,6 +691,23 @@ module Mbeditor
|
|
|
691
691
|
str.match?(SAFE_BRANCH_NAME) ? str : nil
|
|
692
692
|
end
|
|
693
693
|
|
|
694
|
+
def action_cable_enabled?
|
|
695
|
+
return false unless defined?(ActionCable::Channel::Base)
|
|
696
|
+
|
|
697
|
+
mount_path = begin
|
|
698
|
+
ActionCable.server.config.mount_path
|
|
699
|
+
rescue StandardError
|
|
700
|
+
nil
|
|
701
|
+
end
|
|
702
|
+
mount_path = '/cable' if mount_path.blank?
|
|
703
|
+
|
|
704
|
+
Rails.application.routes.routes.any? do |route|
|
|
705
|
+
route.path.spec.to_s.start_with?(mount_path)
|
|
706
|
+
end
|
|
707
|
+
rescue StandardError
|
|
708
|
+
false
|
|
709
|
+
end
|
|
710
|
+
|
|
694
711
|
def allow_missing_file?
|
|
695
712
|
%w[1 true yes on].include?(params[:allow_missing].to_s.downcase)
|
|
696
713
|
end
|
|
@@ -19,10 +19,38 @@ module Mbeditor
|
|
|
19
19
|
# Tagged-logging compat — the block body still passes through the filter.
|
|
20
20
|
def tagged(*tags, &block)
|
|
21
21
|
if __getobj__.respond_to?(:tagged)
|
|
22
|
-
__getobj__.tagged(*tags
|
|
23
|
-
|
|
22
|
+
__getobj__.tagged(*tags, &block)
|
|
23
|
+
elsif block
|
|
24
24
|
block.call
|
|
25
|
+
else
|
|
26
|
+
self
|
|
25
27
|
end
|
|
26
28
|
end
|
|
29
|
+
|
|
30
|
+
# Rails/ActiveSupport logger compatibility. Some logger stacks call these
|
|
31
|
+
# methods even when the underlying logger is not TaggedLogging.
|
|
32
|
+
def current_tags
|
|
33
|
+
return __getobj__.current_tags if __getobj__.respond_to?(:current_tags)
|
|
34
|
+
|
|
35
|
+
[]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def push_tags(*tags)
|
|
39
|
+
return __getobj__.push_tags(*tags) if __getobj__.respond_to?(:push_tags)
|
|
40
|
+
|
|
41
|
+
tags
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def pop_tags(count = 1)
|
|
45
|
+
return __getobj__.pop_tags(count) if __getobj__.respond_to?(:pop_tags)
|
|
46
|
+
|
|
47
|
+
[]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def clear_tags!
|
|
51
|
+
return __getobj__.clear_tags! if __getobj__.respond_to?(:clear_tags!)
|
|
52
|
+
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
27
55
|
end
|
|
28
56
|
end
|
data/lib/mbeditor/version.rb
CHANGED