mbeditor 0.14.0 → 0.14.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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +64 -0
  3. data/app/assets/javascripts/mbeditor/application.js +2 -0
  4. data/app/assets/javascripts/mbeditor/components/CombinedFileView.js +228 -0
  5. data/app/assets/javascripts/mbeditor/components/DiffViewer.js +2 -1
  6. data/app/assets/javascripts/mbeditor/components/EditorPanel.js +18 -1
  7. data/app/assets/javascripts/mbeditor/components/ImportConflictModal.js +60 -5
  8. data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +98 -9
  9. data/app/assets/javascripts/mbeditor/components/PastePlaceModal.js +329 -0
  10. data/app/assets/javascripts/mbeditor/components/ShortcutHelp.js +16 -0
  11. data/app/assets/javascripts/mbeditor/editor_plugins.js +20 -20
  12. data/app/assets/javascripts/mbeditor/file_service.js +10 -0
  13. data/app/assets/javascripts/mbeditor/tab_manager.js +3 -0
  14. data/app/assets/stylesheets/mbeditor/editor.css +33 -1
  15. data/app/assets/stylesheets/mbeditor/glass.css +11 -0
  16. data/app/assets/stylesheets/mbeditor/themes.css +3 -3
  17. data/app/controllers/mbeditor/editors_controller.rb +7 -0
  18. data/app/services/mbeditor/ruby_method_ranges_service.rb +120 -0
  19. data/lib/mbeditor/rack/pending_migration_bypass.rb +16 -1
  20. data/lib/mbeditor/route_map.rb +1 -0
  21. data/lib/mbeditor/version.rb +1 -1
  22. data/public/monaco-editor/VERSIONS +1 -1
  23. data/public/monaco-editor/codicon.ttf +0 -0
  24. data/public/monaco-editor/css.worker.js +41 -37
  25. data/public/monaco-editor/editor.worker.js +19 -15
  26. data/public/monaco-editor/html.worker.js +53 -49
  27. data/public/monaco-editor/json.worker.js +36 -32
  28. data/public/monaco-editor/monaco.css +1 -1
  29. data/public/monaco-editor/monaco.js +388 -301
  30. data/public/monaco-editor/ts.worker.js +226 -222
  31. data/vendor/assets/javascripts/axios.min.js +3 -3
  32. metadata +5 -2
@@ -145,7 +145,7 @@
145
145
  var _discoveredFlushTimer = null;
146
146
  function flushDiscoveredGlobals() {
147
147
  _discoveredFlushTimer = null;
148
- var mts = window.monaco && window.monaco.languages && window.monaco.languages.typescript;
148
+ var mts = window.monaco && window.monaco.typescript;
149
149
  if (!mts || !mts.javascriptDefaults) return;
150
150
  var decls = Object.keys(discoveredJsGlobals)
151
151
  .map(function(k) { return 'declare var ' + k + ': any;'; }).join('\n');
@@ -263,7 +263,7 @@
263
263
  entry.lib.dispose();
264
264
  entry.lib = null;
265
265
  } else if (!open && !entry.lib) {
266
- entry.lib = monaco.languages.typescript.javascriptDefaults
266
+ entry.lib = monaco.typescript.javascriptDefaults
267
267
  .addExtraLib(entry.content, programUri(path));
268
268
  }
269
269
  }
@@ -278,7 +278,7 @@
278
278
 
279
279
  function loadWorkspaceProgram(monaco) {
280
280
  if (typeof FileService === 'undefined') return;
281
- var mts = monaco && monaco.languages && monaco.languages.typescript;
281
+ var mts = monaco && monaco.typescript;
282
282
  if (!mts || !mts.javascriptDefaults) return;
283
283
 
284
284
  var programLoaded = FileService.getJsProgram
@@ -301,7 +301,7 @@
301
301
 
302
302
  function loadWorkspaceGlobals(monaco) {
303
303
  if (typeof FileService === 'undefined' || !FileService.getJsGlobals) return;
304
- var mts = monaco && monaco.languages && monaco.languages.typescript;
304
+ var mts = monaco && monaco.typescript;
305
305
  if (!mts || !mts.javascriptDefaults) return;
306
306
  FileService.getJsGlobals().then(function (data) {
307
307
  if (!data || !data.ok || !data.symbols) return;
@@ -335,7 +335,7 @@
335
335
  // tree. A workspace can be tens of MB, so re-fetching it on every save would
336
336
  // cost more than the feature is worth.
337
337
  function refreshProgramPaths(monaco, paths) {
338
- var mts = monaco && monaco.languages && monaco.languages.typescript;
338
+ var mts = monaco && monaco.typescript;
339
339
  if (!mts || !mts.javascriptDefaults) return;
340
340
  if (typeof FileService === 'undefined' || !FileService.getJsProgramFile) return;
341
341
  var wanted = (paths || []).filter(function (path) {
@@ -1253,8 +1253,8 @@
1253
1253
  function registerJsProviders(monaco) {
1254
1254
  // JavaScript: enable semantic checking (off by default in Monaco) and JSX support.
1255
1255
  // checkJs catches undefined variables, noUnusedLocals catches dead assignments.
1256
- if (monaco.languages.typescript && monaco.languages.typescript.javascriptDefaults) {
1257
- monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
1256
+ if (monaco.typescript && monaco.typescript.javascriptDefaults) {
1257
+ monaco.typescript.javascriptDefaults.setDiagnosticsOptions({
1258
1258
  noSemanticValidation: false,
1259
1259
  noSyntaxValidation: false,
1260
1260
  // Suggestion diagnostics are TypeScript's advice to a TypeScript
@@ -1271,12 +1271,12 @@
1271
1271
  // which is a closed rule rather than a list that grows by one code
1272
1272
  // every time a new false positive turns up.
1273
1273
  });
1274
- monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
1275
- target: monaco.languages.typescript.ScriptTarget.ES2020,
1274
+ monaco.typescript.javascriptDefaults.setCompilerOptions({
1275
+ target: monaco.typescript.ScriptTarget.ES2020,
1276
1276
  allowNonTsExtensions: true,
1277
1277
  allowJs: true,
1278
1278
  checkJs: true,
1279
- jsx: monaco.languages.typescript.JsxEmit.React,
1279
+ jsx: monaco.typescript.JsxEmit.React,
1280
1280
  noUnusedLocals: true
1281
1281
  });
1282
1282
  // Hand JS formatting to Prettier alone.
@@ -1289,8 +1289,8 @@
1289
1289
  // two spaces and never adds semicolons, ignoring every preference. Only
1290
1290
  // formatting is switched off — completions, hovers, diagnostics, rename
1291
1291
  // and the rest of the JS intelligence still come from the worker.
1292
- monaco.languages.typescript.javascriptDefaults.setModeConfiguration(
1293
- Object.assign({}, monaco.languages.typescript.javascriptDefaults.modeConfiguration, {
1292
+ monaco.typescript.javascriptDefaults.setModeConfiguration(
1293
+ Object.assign({}, monaco.typescript.javascriptDefaults.modeConfiguration, {
1294
1294
  documentRangeFormattingEdits: false,
1295
1295
  onTypeFormattingEdits: false
1296
1296
  })
@@ -1401,15 +1401,15 @@
1401
1401
  // window globals from the host application. Common Sprockets globals
1402
1402
  // (React, ReactDOM, etc.) are declared explicitly. For additional globals
1403
1403
  // not auto-detected, add `/* global MyComponent */` at the top of the file.
1404
- if (monaco.languages.typescript && monaco.languages.typescript.javascriptDefaults) {
1405
- monaco.languages.typescript.javascriptDefaults.addExtraLib(
1404
+ if (monaco.typescript && monaco.typescript.javascriptDefaults) {
1405
+ monaco.typescript.javascriptDefaults.addExtraLib(
1406
1406
  REACT_MINI_UMD_DTS,
1407
1407
  'inmemory://mbeditor/react-mini-umd.d.ts'
1408
1408
  );
1409
1409
 
1410
1410
  var dynamicShim = buildWindowGlobalsShim();
1411
1411
  if (dynamicShim) {
1412
- monaco.languages.typescript.javascriptDefaults.addExtraLib(
1412
+ monaco.typescript.javascriptDefaults.addExtraLib(
1413
1413
  dynamicShim,
1414
1414
  'inmemory://mbeditor/window-globals.d.ts'
1415
1415
  );
@@ -1725,11 +1725,11 @@
1725
1725
  }
1726
1726
 
1727
1727
  // TypeScript: enable JSX for .tsx files.
1728
- if (monaco.languages.typescript && monaco.languages.typescript.typescriptDefaults) {
1729
- monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
1730
- target: monaco.languages.typescript.ScriptTarget.ES2020,
1728
+ if (monaco.typescript && monaco.typescript.typescriptDefaults) {
1729
+ monaco.typescript.typescriptDefaults.setCompilerOptions({
1730
+ target: monaco.typescript.ScriptTarget.ES2020,
1731
1731
  allowNonTsExtensions: true,
1732
- jsx: monaco.languages.typescript.JsxEmit.React,
1732
+ jsx: monaco.typescript.JsxEmit.React,
1733
1733
  noUnusedLocals: true
1734
1734
  });
1735
1735
  }
@@ -1932,7 +1932,7 @@
1932
1932
  function collectComponentProps(monaco, model, name) {
1933
1933
  var sources = [model.getValue()];
1934
1934
  try {
1935
- var libs = monaco.languages.typescript.javascriptDefaults.getExtraLibs();
1935
+ var libs = monaco.typescript.javascriptDefaults.getExtraLibs();
1936
1936
  Object.keys(libs).forEach(function(uri) {
1937
1937
  if (libs[uri] && libs[uri].content) sources.push(libs[uri].content);
1938
1938
  });
@@ -304,6 +304,15 @@ var FileService = (function () {
304
304
  .then(function (res) { return res.data; });
305
305
  }
306
306
 
307
+ // Method line ranges of Ruby source (code).
308
+ // Resolves { methods, owners } or { error } — a syntax error is data, not a throw.
309
+ function getRubyMethods(code) {
310
+ return axios.post(window.mbeditorBasePath() + '/ruby_methods', { code: code }).then(function (res) { return res.data; }, function (err) {
311
+ if (err && err.response && err.response.data && err.response.data.error && err.response.status === 422) return { error: err.response.data.error };
312
+ throw err;
313
+ });
314
+ }
315
+
307
316
  function getState() {
308
317
  return axios.get(window.mbeditorBasePath() + '/state').then(function(res) { return res.data; });
309
318
  }
@@ -550,6 +559,7 @@ var FileService = (function () {
550
559
  runTests: runTests,
551
560
  ping: ping,
552
561
  getRoutes: getRoutes,
562
+ getRubyMethods: getRubyMethods,
553
563
  isServerReachable: ServerReachability.isOnline,
554
564
  onReachabilityChange: ServerReachability.onChange,
555
565
  getState: getState,
@@ -620,6 +620,9 @@ var TabManager = (function () {
620
620
  var state = EditorStore.getState();
621
621
  var pane = state.panes.find(function(p) { return p.id === paneId; });
622
622
  var newActive = pane ? pane.activeTabId : null;
623
+ // Every click in a pane lands here; a no-op setState still re-renders the
624
+ // app, which mid-click resets controlled inputs (checkboxes) to their old value.
625
+ if (state.focusedPaneId === paneId && state.activeTabId === newActive) return;
623
626
  EditorStore.setState({ focusedPaneId: paneId, activeTabId: newActive });
624
627
  }
625
628
 
@@ -128,6 +128,10 @@ html, body, #mbeditor-root {
128
128
  }
129
129
 
130
130
  .ide-titlebar {
131
+ /* .statusbar-btn paints from --ide-accent-text, which is "text on the accent
132
+ fill" in the legacy themes. The title bar is panel-coloured in every theme,
133
+ so its buttons read the ordinary text colour instead. */
134
+ --ide-accent-text: var(--ide-text);
131
135
  display: flex;
132
136
  align-items: center;
133
137
  background: var(--ide-panel-bg);
@@ -1961,7 +1965,7 @@ body.mbeditor-migration-pending .ide-statusbar {
1961
1965
  .changelog-version-badge {
1962
1966
  font-size: 15px;
1963
1967
  font-weight: 700;
1964
- color: var(--ide-accent-text, #4fc1ff);
1968
+ color: var(--ide-accent-fg);
1965
1969
  letter-spacing: -0.01em;
1966
1970
  }
1967
1971
  .changelog-version-date {
@@ -2363,6 +2367,8 @@ body.mbeditor-migration-pending .ide-statusbar {
2363
2367
  /* Nothing to click, so nothing that looks clickable. */
2364
2368
  .collapsible-header-static { cursor: default; }
2365
2369
  .collapsible-header-static:hover { background: none; }
2370
+ /* A sticky header still needs its own fill, or scrolled rows show through it. */
2371
+ .ide-sidebar-scrollable .collapsible-header-static:hover { background: var(--ide-panel-bg); }
2366
2372
 
2367
2373
 
2368
2374
  .collapsible-toggle {
@@ -4135,3 +4141,29 @@ input.ide-model-graph-search[type="search"] {
4135
4141
  .mbeditor-collab-diag-label { font-weight: 500; }
4136
4142
  .mbeditor-collab-diag-detail { color: var(--ide-text-muted, #858585); margin-top: 3px; line-height: 1.5; }
4137
4143
  .mbeditor-collab-diag-foot { color: var(--ide-text-muted, #858585); margin-bottom: 12px; }
4144
+
4145
+ .combined-view { height: 100%; overflow-y: auto; background: var(--ide-bg, #1e1e1e); color: var(--ide-text, #d4d4d4); }
4146
+ .combined-note { padding: 8px 16px; font-size: 12px; color: var(--ide-text-muted, #888); }
4147
+ .combined-file-header { position: sticky; top: 0; z-index: 1; display: flex; align-items: center; gap: 8px; padding: 6px 16px; background: var(--ide-bg-elevated, var(--ide-bg, #252526)); border-bottom: 1px solid var(--ide-border, #333); font-size: 12px; }
4148
+ .combined-file-path { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
4149
+ .combined-file-btn { font-size: 11px; padding: 2px 8px; cursor: pointer; color: inherit; background: transparent; border: 1px solid var(--ide-border, #444); border-radius: 3px; }
4150
+ .combined-file-btn:disabled { opacity: 0.4; cursor: default; }
4151
+ .combined-file-body { margin: 0; padding: 8px 16px 24px; overflow-x: auto; font: 12px/1.5 Menlo, Consolas, monospace; tab-size: 2; }
4152
+
4153
+ /* Combined view: method picking + Markdown export; import Compare; Paste & Place */
4154
+ .combined-toolbar { display: flex; gap: 8px; padding: 6px 16px; border-bottom: 1px solid var(--ide-border, #333); }
4155
+ .combined-picker { max-height: 30vh; overflow-y: auto; padding: 4px 16px; font-size: 12px; border-bottom: 1px solid var(--ide-border, #333); }
4156
+ .combined-pick-file, .combined-pick-method { display: flex; align-items: center; gap: 6px; white-space: nowrap; cursor: pointer; }
4157
+ .combined-pick-method { padding-left: 22px; font-size: 11px; }
4158
+ .import-conflict-compare { margin-left: 8px; padding: 0; background: none; border: 0; color: var(--ide-accent, #0e639c); font-size: 11px; cursor: pointer; }
4159
+ .import-conflict-compare:hover { text-decoration: underline; }
4160
+ .import-conflict-modal.import-conflict-wide { min-width: min(900px, 94vw); max-width: 94vw; }
4161
+ .import-conflict-diff { height: 360px; margin-top: 8px; }
4162
+ .paste-place-modal { min-width: min(900px, 94vw); max-width: 94vw; }
4163
+ .paste-place-body { padding: 8px 16px; }
4164
+ .paste-place-input { width: 100%; box-sizing: border-box; font: 12px/1.5 var(--ide-mono-font, monospace); color: var(--ide-text, #d4d4d4); background: var(--ide-input-bg, #3c3c3c); border: 1px solid var(--ide-border, #3c3c3c); border-radius: 3px; padding: 8px; resize: vertical; }
4165
+ .paste-place-error { color: var(--ide-error, #f48771); font-size: 12px; padding: 6px 0; }
4166
+ .paste-place-row { display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: 12px; }
4167
+ .paste-place-name { font-family: var(--ide-mono-font, monospace); min-width: 160px; }
4168
+ .paste-place-row select { flex: 1; min-width: 0; color: var(--ide-text, #d4d4d4); background: var(--ide-input-bg, #3c3c3c); border: 1px solid var(--ide-border, #3c3c3c); }
4169
+ .paste-place-diff { height: 260px; margin-top: 8px; border: 1px solid var(--ide-border, #3c3c3c); }
@@ -117,6 +117,17 @@ html[data-glass] .ide-editor-wrapper .monaco-editor .margin { background-color:
117
117
  /* Taller than Modern's 22px so the chips have room; the 6px gutter above it
118
118
  stays, matching the explorer gutter, and it eats 2px of shell padding below. */
119
119
  html[data-glass] .ide-statusbar { border-top: 1px solid var(--g-edge); height: 28px; margin: 0 0 -2px; }
120
+ /* The bar is a panel-coloured card here, not the accent fill the legacy themes
121
+ paint flat, so its text is the ordinary text colour. While migrations are
122
+ pending it is amber instead, and the warn pair is what stays readable on it:
123
+ the base .ide-statusbar background is overridden by the glass surface above,
124
+ so the warn background has to be restated. */
125
+ html[data-glass] .ide-statusbar { --ide-accent-text: var(--ide-text); }
126
+ html[data-glass] body.mbeditor-migration-pending .ide-statusbar {
127
+ background: var(--ide-statusbar-warn-bg);
128
+ --ide-accent-text: var(--ide-statusbar-warn-text);
129
+ --ide-text-muted: var(--ide-statusbar-warn-text);
130
+ }
120
131
  /* The drawer-open handle lies over the 6px gutter between the editor card and
121
132
  the status bar rather than adding a strip of its own above it. */
122
133
  /* Sits in the shell's own 6px gap between the body and the status bar. */
@@ -162,7 +162,7 @@
162
162
  /* ══ DRACULA ════════════════════════════════════════════════════════════════ */
163
163
  [data-theme="dracula"] {
164
164
  color-scheme: dark;
165
- --ide-accent-text: #ffffff;
165
+ --ide-accent-text: #282a36;
166
166
  --ide-statusbar-warn-bg: #ffb86c;
167
167
  --ide-statusbar-warn-text: #282a36;
168
168
  --status-hover-bg: rgba(255,255,255,0.08);
@@ -194,7 +194,7 @@
194
194
  /* ══ NIGHT OWL ══════════════════════════════════════════════════════════════ */
195
195
  [data-theme="night-owl"] {
196
196
  color-scheme: dark;
197
- --ide-accent-text: #ffffff;
197
+ --ide-accent-text: #011627;
198
198
  --ide-statusbar-warn-bg: #ffcb8b;
199
199
  --ide-statusbar-warn-text: #011627;
200
200
  --status-hover-bg: rgba(255,255,255,0.08);
@@ -322,7 +322,7 @@
322
322
  /* ══ TOMORROW NIGHT ════════════════════════════════════════════════════════ */
323
323
  [data-theme="tomorrow-night"] {
324
324
  color-scheme: dark;
325
- --ide-accent-text: #ffffff;
325
+ --ide-accent-text: #1d1f21;
326
326
  --ide-statusbar-warn-bg: #de935f;
327
327
  --ide-statusbar-warn-text: #1d1f21;
328
328
  --status-hover-bg: rgba(255,255,255,0.08);
@@ -758,6 +758,13 @@ module Mbeditor
758
758
  status: :unprocessable_content
759
759
  end
760
760
 
761
+ # POST /mbeditor/ruby_methods {code:} — method line ranges of Ruby source.
762
+ # A syntax error is a 422 with {error:}, never a raise.
763
+ def ruby_methods
764
+ result = RubyMethodRangesService.call(params[:code].to_s)
765
+ render json: result, status: result[:error] ? :unprocessable_content : :ok
766
+ end
767
+
761
768
  # GET /mbeditor/exceptions — recorded host-app exceptions, newest first.
762
769
  #
763
770
  # The cable push is the live path; this seeds the panel on load and covers
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ripper"
4
+
5
+ module Mbeditor
6
+ # Line ranges of every method (and enclosing class/module) in Ruby source.
7
+ #
8
+ # Ripper rather than Prism: it is stdlib on every Ruby this gem supports.
9
+ # Events fire bottom-up, so a node's end line is the last significant token
10
+ # the scanner produced before it reduced, and owners are resolved afterwards
11
+ # by range containment instead of a stack.
12
+ #
13
+ # call(src) => { methods: [{ name:, singleton:, owner:, start_line:,
14
+ # doc_start_line:, end_line: }],
15
+ # owners: [{ kind:, name:, start_line:, end_line: }] }
16
+ # | { error: "..." } (owners also hold `class << self` as kind "sclass")
17
+ class RubyMethodRangesService
18
+ MAX_BYTES = 200 * 1024
19
+
20
+ # Tokens that carry no code; a def never ends on one of these.
21
+ BLANK = %i[sp nl ignored_nl comment embdoc_beg embdoc embdoc_end words_sep ignored_sp].freeze
22
+
23
+ class Collector < Ripper::SexpBuilder
24
+ attr_reader :methods_found, :owners_found
25
+
26
+ def initialize(*)
27
+ super
28
+ @methods_found = []
29
+ @owners_found = []
30
+ @last_line = 1
31
+ end
32
+
33
+ (Ripper::SCANNER_EVENTS - BLANK).each do |ev|
34
+ define_method(:"on_#{ev}") do |tok|
35
+ @last_line = lineno
36
+ super(tok)
37
+ end
38
+ end
39
+
40
+ def on_def(name, *rest)
41
+ record(name, false)
42
+ super
43
+ end
44
+
45
+ def on_defs(target, period, name, *rest)
46
+ record(name, true)
47
+ super
48
+ end
49
+
50
+ def on_class(const, *rest)
51
+ owner(:class, const)
52
+ super
53
+ end
54
+
55
+ def on_sclass(target, *rest)
56
+ @owners_found << { kind: "sclass", name: "<< self", start_line: first_line(target), end_line: @last_line }
57
+ super
58
+ end
59
+
60
+ def on_module(const, *rest)
61
+ owner(:module, const)
62
+ super
63
+ end
64
+
65
+ private
66
+
67
+ def record(ident, singleton)
68
+ @methods_found << { name: ident[1], singleton: singleton, start_line: ident[2][0], end_line: @last_line }
69
+ end
70
+
71
+ def owner(kind, const)
72
+ @owners_found << { kind: kind.to_s, name: const_name(const), start_line: first_line(const), end_line: @last_line }
73
+ end
74
+
75
+ def const_name(node)
76
+ return "" unless node.is_a?(Array)
77
+
78
+ case node[0]
79
+ when :@const then node[1]
80
+ when :const_ref, :var_ref, :top_const_ref then const_name(node[1])
81
+ when :const_path_ref then "#{const_name(node[1])}::#{const_name(node[2])}"
82
+ else ""
83
+ end
84
+ end
85
+
86
+ def first_line(node)
87
+ return node[2][0] if node.is_a?(Array) && node[0].is_a?(Symbol) && node[0].to_s.start_with?("@")
88
+
89
+ node.each { |c| l = first_line(c) if c.is_a?(Array); return l if l } if node.is_a?(Array)
90
+ nil
91
+ end
92
+ end
93
+
94
+ def self.call(source)
95
+ source = source.to_s
96
+ return { error: "Source too large" } if source.bytesize > MAX_BYTES
97
+
98
+ parser = Collector.new(source)
99
+ parser.parse
100
+ return { error: "Syntax error" } if parser.error?
101
+
102
+ lines = source.lines
103
+ owners = parser.owners_found.sort_by { |o| [o[:start_line], -o[:end_line]] }
104
+ methods = parser.methods_found.sort_by { |m| m[:start_line] }.map do |m|
105
+ chain = owners.select { |o| o[:start_line] <= m[:start_line] && o[:end_line] >= m[:end_line] }
106
+ m.merge(singleton: m[:singleton] || chain.any? { |o| o[:kind] == "sclass" },
107
+ owner: chain.reject { |o| o[:kind] == "sclass" }.map { |o| o[:name] }.join("::"), doc_start_line: doc_start(lines, m[:start_line]))
108
+ end
109
+ { methods: methods, owners: owners }
110
+ rescue StandardError, SystemStackError => e
111
+ { error: e.message }
112
+ end
113
+
114
+ def self.doc_start(lines, start_line)
115
+ line = start_line
116
+ line -= 1 while line > 1 && lines[line - 2].to_s.lstrip.start_with?("#")
117
+ line
118
+ end
119
+ end
120
+ end
@@ -33,6 +33,7 @@ module Mbeditor
33
33
  # is still told to migrate — they are just no longer prevented from working.
34
34
  class PendingMigrationBypass
35
35
  DEFAULT_ASSET_PREFIX = "/assets"
36
+ DEFAULT_CABLE_PATH = "/cable"
36
37
 
37
38
  def initialize(app, file_watcher: nil)
38
39
  @app = app
@@ -94,7 +95,21 @@ module Mbeditor
94
95
  prefix = Mbeditor::MountPath.resolve
95
96
  return true if path == prefix || path.start_with?("#{prefix}/")
96
97
 
97
- path.start_with?("#{asset_prefix}/")
98
+ return true if path.start_with?("#{asset_prefix}/")
99
+
100
+ # The cable carries presence, collaboration and file-change push. Left
101
+ # blocked it 500s on every reconnect, and the client's status chips
102
+ # flicker for as long as the migration stays pending.
103
+ path == cable_path || path.start_with?("#{cable_path}/")
104
+ end
105
+
106
+ def cable_path
107
+ @cable_path ||= begin
108
+ configured = Rails.application.config.action_cable.mount_path if Rails.application.config.respond_to?(:action_cable)
109
+ (configured.presence || DEFAULT_CABLE_PATH).to_s.chomp("/")
110
+ rescue StandardError
111
+ DEFAULT_CABLE_PATH
112
+ end
98
113
  end
99
114
 
100
115
  def asset_prefix
@@ -37,6 +37,7 @@ module Mbeditor
37
37
  get 'js_program', to: 'editors#js_program'
38
38
  post 'ruby_lsp', to: 'editors#ruby_lsp'
39
39
  post 'ruby_rename', to: 'editors#ruby_rename'
40
+ post 'ruby_methods', to: 'editors#ruby_methods'
40
41
  get 'model_graph', to: 'editors#model_graph'
41
42
  get 'exceptions', to: 'editors#exceptions'
42
43
  delete 'exceptions', to: 'editors#clear_exceptions'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mbeditor
4
- VERSION = "0.14.0"
4
+ VERSION = "0.14.2"
5
5
  end
@@ -1,3 +1,3 @@
1
- monaco-editor 0.55.1
1
+ monaco-editor 0.57.0
2
2
  monaco-vim 0.4.4
3
3
  esbuild 0.28.0
Binary file