mbeditor 0.14.0 → 0.14.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.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -0
  3. data/app/assets/javascripts/mbeditor/application.js +2 -0
  4. data/app/assets/javascripts/mbeditor/components/CombinedFileView.js +214 -0
  5. data/app/assets/javascripts/mbeditor/components/EditorPanel.js +13 -1
  6. data/app/assets/javascripts/mbeditor/components/ImportConflictModal.js +60 -5
  7. data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +68 -8
  8. data/app/assets/javascripts/mbeditor/components/PastePlaceModal.js +329 -0
  9. data/app/assets/javascripts/mbeditor/components/ShortcutHelp.js +16 -0
  10. data/app/assets/javascripts/mbeditor/editor_plugins.js +20 -20
  11. data/app/assets/javascripts/mbeditor/file_service.js +10 -0
  12. data/app/assets/stylesheets/mbeditor/editor.css +30 -1
  13. data/app/assets/stylesheets/mbeditor/glass.css +11 -0
  14. data/app/assets/stylesheets/mbeditor/themes.css +3 -3
  15. data/app/controllers/mbeditor/editors_controller.rb +7 -0
  16. data/app/services/mbeditor/ruby_method_ranges_service.rb +120 -0
  17. data/lib/mbeditor/rack/pending_migration_bypass.rb +16 -1
  18. data/lib/mbeditor/route_map.rb +1 -0
  19. data/lib/mbeditor/version.rb +1 -1
  20. data/public/monaco-editor/VERSIONS +1 -1
  21. data/public/monaco-editor/codicon.ttf +0 -0
  22. data/public/monaco-editor/css.worker.js +41 -37
  23. data/public/monaco-editor/editor.worker.js +19 -15
  24. data/public/monaco-editor/html.worker.js +53 -49
  25. data/public/monaco-editor/json.worker.js +36 -32
  26. data/public/monaco-editor/monaco.css +1 -1
  27. data/public/monaco-editor/monaco.js +388 -301
  28. data/public/monaco-editor/ts.worker.js +226 -222
  29. data/vendor/assets/javascripts/axios.min.js +3 -3
  30. metadata +5 -2
@@ -0,0 +1,329 @@
1
+ 'use strict';
2
+
3
+ // Paste & Place (Ruby). The user pastes one or more methods (optionally inside
4
+ // their `class Foo ... end`); each is matched to the method it should replace,
5
+ // or offered as a new method. Nothing is written to disk: Apply pushes undoable
6
+ // edits into the live Monaco buffer, which marks the tab dirty.
7
+ //
8
+ // Line ranges come from POST /ruby_methods (Ripper), computed on the *buffer*
9
+ // text, so they match what the editor holds rather than what is on disk.
10
+
11
+ var PastePlaceModal = (function () {
12
+ var useState = React.useState;
13
+ var useRef = React.useRef;
14
+ var useMemo = React.useMemo;
15
+
16
+ var MAX_CANDIDATE_FILES = 10;
17
+
18
+ function indentOf(line) { return /^[ \t]*/.exec(line || '')[0]; }
19
+
20
+ function escapeRegex(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
21
+
22
+ function activeRubyPath() {
23
+ var st = EditorStore.getState();
24
+ var pane = st.panes.find(function (p) { return p.id === st.focusedPaneId; }) || st.panes[0];
25
+ var tab = pane && pane.tabs.find(function (t) { return t.id === pane.activeTabId; });
26
+ return tab && tab.path && /\.rb$/.test(tab.path) && !/^[a-z-]+:\/\//.test(tab.path) ? tab.path : null;
27
+ }
28
+
29
+ // Live buffer when the file is open, disk otherwise.
30
+ function bufferText(path) {
31
+ var entry = window.__mbeditorModels && window.__mbeditorModels[path];
32
+ if (entry && entry.model && !entry.model.isDisposed()) return Promise.resolve(entry.model.getValue());
33
+ return FileService.getFile(path).then(function (d) {
34
+ if (!d || typeof d.content !== 'string') throw new Error('unreadable');
35
+ return d.content;
36
+ });
37
+ }
38
+
39
+ function ownerPath(owners, o) {
40
+ return owners.filter(function (x) {
41
+ return x.kind !== 'sclass' && x.start_line <= o.start_line && x.end_line >= o.end_line;
42
+ }).map(function (x) { return x.name; }).join('::');
43
+ }
44
+
45
+ function sameMethod(tm, m) {
46
+ return tm.name === m.name && tm.singleton === m.singleton &&
47
+ (!m.owner || tm.owner === m.owner || tm.owner.slice(-(m.owner.length + 2)) === '::' + m.owner);
48
+ }
49
+
50
+ // The class/module whose closing `end` receives a new method.
51
+ function insertionOwner(info, snippetOwner) {
52
+ var real = info.owners.filter(function (o) { return o.kind !== 'sclass'; });
53
+ if (snippetOwner) {
54
+ return real.filter(function (o) {
55
+ var full = ownerPath(info.owners, o);
56
+ return full === snippetOwner || full.slice(-(snippetOwner.length + 2)) === '::' + snippetOwner;
57
+ })[0] || null;
58
+ }
59
+ // ponytail: first top-level class/module; an owner-less snippet in a file with none is left unplaced.
60
+ return real.filter(function (o) { return o.kind === 'class'; })[0] || real[0] || null;
61
+ }
62
+
63
+ function reindent(snippetLines, base, target) {
64
+ return snippetLines.map(function (l) {
65
+ if (l.trim() === '') return '';
66
+ return target + (l.indexOf(base) === 0 ? l.slice(base.length) : l.replace(/^[ \t]+/, ''));
67
+ });
68
+ }
69
+
70
+ // Everything async: parse the snippet, then find targets for every method.
71
+ function findPlacement(code) {
72
+ var cache = {};
73
+ function rangesFor(path) {
74
+ if (!cache[path]) {
75
+ cache[path] = bufferText(path).then(function (text) {
76
+ return FileService.getRubyMethods(text).then(function (info) {
77
+ if (info.error) throw new Error(info.error);
78
+ return { text: text, info: info };
79
+ });
80
+ });
81
+ }
82
+ return cache[path];
83
+ }
84
+
85
+ return FileService.getRubyMethods(code).then(function (snip) {
86
+ if (snip.error) throw new Error('Snippet is not valid Ruby: ' + snip.error);
87
+ if (!snip.methods.length) throw new Error('No methods found in the pasted code.');
88
+ var lines = code.split('\n');
89
+ var active = activeRubyPath();
90
+
91
+ // One method at a time: /search supersedes (kills) a search still in flight.
92
+ var items = [];
93
+ return snip.methods.reduce(function (chain, m) { return chain.then(function () { return placeOne(m).then(function (item) { items.push(item); }); }); }, Promise.resolve())
94
+ .then(function () { return { items: items, texts: cache }; });
95
+
96
+ function placeOne(m) {
97
+ var item = {
98
+ m: m,
99
+ hasDoc: m.doc_start_line < m.start_line,
100
+ lines: lines.slice(m.doc_start_line - 1, m.end_line),
101
+ base: indentOf(lines[m.start_line - 1]),
102
+ options: []
103
+ };
104
+ var existing = [];
105
+ var step = Promise.resolve();
106
+ if (active) {
107
+ step = rangesFor(active).then(function (r) {
108
+ var t = r.info.methods.filter(function (tm) { return sameMethod(tm, m); })[0];
109
+ if (t) existing.push({ path: active, target: t });
110
+ }, function () {});
111
+ }
112
+ return step.then(function () {
113
+ if (existing.length || !m.name) return;
114
+ var q = 'def[[:space:]]+' + (m.singleton ? 'self\\.' : '') + escapeRegex(m.name);
115
+ return axios.get(window.mbeditorBasePath() + '/search', { params: { q: q, regex: 'true', limit: 50 } })
116
+ .then(function (res) {
117
+ var seen = {};
118
+ var files = ((res.data && res.data.results) || []).map(function (r) { return r.file; })
119
+ .filter(function (f) {
120
+ if (!/\.rb$/.test(f) || f === active || seen[f]) return false;
121
+ return (seen[f] = true);
122
+ }).slice(0, MAX_CANDIDATE_FILES);
123
+ return Promise.all(files.map(function (f) {
124
+ return rangesFor(f).then(function (r) {
125
+ var t = r.info.methods.filter(function (tm) { return sameMethod(tm, m); })[0];
126
+ if (t) existing.push({ path: f, target: t });
127
+ }, function () {});
128
+ }));
129
+ }, function () {});
130
+ }).then(function () {
131
+ item.options = existing.map(function (c) {
132
+ return { kind: 'replace', path: c.path, target: c.target, label: c.path + ':' + c.target.start_line };
133
+ });
134
+ var offerInsert = function (path) {
135
+ return rangesFor(path).then(function (r) {
136
+ var owner = insertionOwner(r.info, m.owner);
137
+ if (owner) item.options.push({ kind: 'insert', path: path, owner: owner, label: 'Insert as new method in ' + path });
138
+ return !!owner;
139
+ }, function () { return false; });
140
+ };
141
+ return (active ? offerInsert(active) : Promise.resolve(false)).then(function (placed) {
142
+ if (placed || !m.owner) return;
143
+ // A new method for a class that lives in another file goes into that file.
144
+ var q = '(class|module)[[:space:]]+' + escapeRegex(m.owner.split('::').pop()) + '([^A-Za-z0-9_]|$)';
145
+ return axios.get(window.mbeditorBasePath() + '/search', { params: { q: q, regex: 'true', limit: 20 } })
146
+ .then(function (res) {
147
+ var files = ((res.data && res.data.results) || []).map(function (r) { return r.file; })
148
+ .filter(function (f, i, all) { return /\.rb$/.test(f) && f !== active && all.indexOf(f) === i; })
149
+ .slice(0, MAX_CANDIDATE_FILES);
150
+ return Promise.all(files.map(offerInsert));
151
+ }, function () {});
152
+ });
153
+ }).then(function () {
154
+ item.options.push({ kind: 'skip', label: 'Skip' });
155
+ return item;
156
+ });
157
+ }
158
+ });
159
+ }
160
+
161
+ // Edits are expressed in 1-based line numbers against the text they were
162
+ // planned on: replace = lines start..end inclusive, insert = before line `at`.
163
+ function buildEdits(items, choices, texts) {
164
+ var byFile = {};
165
+ items.forEach(function (item, i) {
166
+ var opt = item.options[choices[i] == null ? 0 : choices[i]];
167
+ if (!opt || opt.kind === 'skip') return;
168
+ var entry = texts[opt.path];
169
+ var lines = entry.text.split('\n');
170
+ var f = byFile[opt.path] || (byFile[opt.path] = { text: entry.text, edits: [] });
171
+ if (opt.kind === 'replace') {
172
+ var t = opt.target;
173
+ f.edits.push({
174
+ start: item.hasDoc ? t.doc_start_line : t.start_line, end: t.end_line,
175
+ lines: reindent(item.lines, item.base, indentOf(lines[t.start_line - 1]))
176
+ });
177
+ } else {
178
+ var unit = /^\t/m.test(entry.text) ? '\t' : ' ';
179
+ var at = opt.owner.end_line;
180
+ var indent = indentOf(lines[at - 1]) + unit;
181
+ // Pasted methods are public: landing after `private` would hide a controller action.
182
+ for (var v = opt.owner.start_line; v < opt.owner.end_line - 1; v++) {
183
+ if (/^\s*(private|protected)\s*(#.*)?$/.test(lines[v])) { at = v + 1; indent = indentOf(lines[v]); break; }
184
+ }
185
+ var prev = lines[at - 2];
186
+ var body = reindent(item.lines, item.base, indent);
187
+ if (at !== opt.owner.end_line) body.push('');
188
+ if (at - 1 !== opt.owner.start_line && prev && prev.trim() !== '') body.unshift('');
189
+ f.edits.push({ start: at, end: at - 1, lines: body });
190
+ }
191
+ });
192
+ return byFile;
193
+ }
194
+
195
+ function applyToText(text, edits) {
196
+ var lines = text.split('\n');
197
+ edits.slice().sort(function (a, b) { return b.start - a.start; }).forEach(function (e) {
198
+ lines.splice.apply(lines, [e.start - 1, e.end - e.start + 1].concat(e.lines));
199
+ });
200
+ return lines.join('\n');
201
+ }
202
+
203
+ function waitForModel(path) {
204
+ var tries = 0;
205
+ return new Promise(function (resolve, reject) {
206
+ (function poll() {
207
+ var e = window.__mbeditorModels && window.__mbeditorModels[path];
208
+ if (e && e.model && !e.model.isDisposed() && e.cleanVersionId != null) return resolve(e.model);
209
+ if (++tries > 100) return reject(new Error('Could not open ' + path));
210
+ setTimeout(poll, 50);
211
+ })();
212
+ });
213
+ }
214
+
215
+ function applyEdits(byFile) {
216
+ var paths = Object.keys(byFile);
217
+ paths.forEach(function (p) { TabManager.openTab(p, p.split('/').pop()); });
218
+ return Promise.all(paths.map(waitForModel)).then(function (models) {
219
+ models.forEach(function (model, i) {
220
+ if (model.getValue().replace(/\r\n/g, '\n') !== byFile[paths[i]].text.replace(/\r\n/g, '\n')) {
221
+ throw new Error(paths[i] + ' changed since the preview. Run Find placement again.');
222
+ }
223
+ });
224
+ models.forEach(function (model, i) {
225
+ var eol = model.getEOL();
226
+ var ops = byFile[paths[i]].edits.map(function (e) {
227
+ var range = e.end < e.start
228
+ ? new window.monaco.Range(e.start, 1, e.start, 1)
229
+ : new window.monaco.Range(e.start, 1, e.end, model.getLineMaxColumn(e.end));
230
+ return { range: range, text: e.end < e.start ? e.lines.join(eol) + eol : e.lines.join(eol) };
231
+ });
232
+ model.pushStackElement();
233
+ model.pushEditOperations([], ops, function () { return null; });
234
+ model.pushStackElement();
235
+ });
236
+ });
237
+ }
238
+
239
+ var Modal = function PastePlaceModal(props) {
240
+ var modalRef = useRef(null);
241
+ var _c = useState('');
242
+ var code = _c[0]; var setCode = _c[1];
243
+ var _p = useState(null);
244
+ var plan = _p[0]; var setPlan = _p[1];
245
+ var _ch = useState({});
246
+ var choices = _ch[0]; var setChoices = _ch[1];
247
+ var _b = useState(false);
248
+ var busy = _b[0]; var setBusy = _b[1];
249
+ var _e = useState(null);
250
+ var error = _e[0]; var setError = _e[1];
251
+
252
+ window.useModalFocusTrap(modalRef, props.onClose);
253
+
254
+ var byFile = useMemo(function () {
255
+ return plan ? buildEdits(plan.items, choices, plan.texts) : {};
256
+ }, [plan, choices]);
257
+ var files = Object.keys(byFile);
258
+
259
+ var find = function () {
260
+ setBusy(true); setError(null);
261
+ findPlacement(code).then(function (p) {
262
+ // texts holds promises; settle them into { text, info } for synchronous use.
263
+ var paths = Object.keys(p.texts);
264
+ return Promise.all(paths.map(function (k) { return p.texts[k]; })).then(function (vals) {
265
+ var settled = {};
266
+ paths.forEach(function (k, i) { settled[k] = vals[i]; });
267
+ setPlan({ items: p.items, texts: settled });
268
+ setChoices({});
269
+ });
270
+ }).then(function () { setBusy(false); }, function (err) { setBusy(false); setError(err.message || 'Failed'); });
271
+ };
272
+
273
+ var apply = function () {
274
+ setBusy(true); setError(null);
275
+ applyEdits(Object.keys(byFile).reduce(function (acc, p) {
276
+ acc[p] = { text: byFile[p].text, edits: byFile[p].edits };
277
+ return acc;
278
+ }, {})).then(function () {
279
+ EditorStore.setStatus('Pasted into ' + files.length + ' file' + (files.length === 1 ? '' : 's') + ' (unsaved)', 'info');
280
+ props.onClose();
281
+ }, function (err) { setBusy(false); setError(err.message || 'Failed'); });
282
+ };
283
+
284
+ var h = React.createElement;
285
+ return h('div', { className: 'schema-modal-overlay', onClick: props.onClose },
286
+ h('div', {
287
+ ref: modalRef, className: 'schema-modal paste-place-modal', role: 'dialog', 'aria-modal': 'true',
288
+ 'aria-label': 'Paste and place', tabIndex: -1, onClick: function (e) { e.stopPropagation(); }
289
+ },
290
+ h('div', { className: 'schema-modal-header' }, h('div', { className: 'schema-modal-title' }, 'Paste & Place')),
291
+ h('div', { className: 'schema-modal-body paste-place-body' },
292
+ !plan && h('textarea', {
293
+ className: 'paste-place-input', value: code, spellCheck: false, rows: 14,
294
+ placeholder: 'Paste Ruby methods here, or a class/module that wraps them.',
295
+ onChange: function (e) { setCode(e.target.value); }
296
+ }),
297
+ error && h('div', { className: 'paste-place-error' }, error),
298
+ plan && plan.items.map(function (item, i) {
299
+ return h('div', { key: i, className: 'paste-place-row' },
300
+ h('span', { className: 'paste-place-name' }, (item.m.owner ? item.m.owner : '') + (item.m.singleton ? '.' : '#') + item.m.name),
301
+ h('select', {
302
+ value: choices[i] == null ? 0 : choices[i],
303
+ onChange: function (e) { var v = +e.target.value; setChoices(function (prev) { var o = Object.assign({}, prev); o[i] = v; return o; }); }
304
+ }, item.options.map(function (o, k) { return h('option', { key: k, value: k }, o.label); }))
305
+ );
306
+ }),
307
+ plan && files.map(function (p) {
308
+ return h('div', { key: p, className: 'paste-place-diff' },
309
+ h(window.DiffViewer, {
310
+ path: p, original: byFile[p].text, modified: applyToText(byFile[p].text, byFile[p].edits),
311
+ isDark: props.isDark, editorPrefs: props.editorPrefs
312
+ }));
313
+ }),
314
+ plan && files.length === 0 && h('div', { className: 'combined-note' }, 'Nothing to apply.')
315
+ ),
316
+ h('div', { className: 'import-conflict-actions' },
317
+ h('button', { type: 'button', className: 'import-conflict-btn', onClick: plan ? function () { setPlan(null); setError(null); } : props.onClose }, plan ? 'Back' : 'Cancel'),
318
+ plan
319
+ ? h('button', { type: 'button', className: 'import-conflict-btn import-conflict-btn-primary', disabled: busy || files.length === 0, onClick: apply }, 'Apply to editor')
320
+ : h('button', { type: 'button', className: 'import-conflict-btn import-conflict-btn-primary', disabled: busy || !code.trim(), onClick: find }, busy ? 'Searching...' : 'Find placement')
321
+ )
322
+ )
323
+ );
324
+ };
325
+
326
+ return Modal;
327
+ })();
328
+
329
+ window.PastePlaceModal = PastePlaceModal;
@@ -182,6 +182,22 @@ var ShortcutHelp = function ShortcutHelp(_ref) {
182
182
  ]
183
183
  }
184
184
  )
185
+ ),
186
+
187
+ React.createElement(
188
+ Section,
189
+ { title: 'Sharing code with a chat assistant' },
190
+ React.createElement(
191
+ Tips,
192
+ {
193
+ items: [
194
+ 'Combined View: Ctrl/Cmd-click several files or folders, right-click, "Open in Combined View" — every file on one page with Copy and Download',
195
+ 'Pick methods: in Combined View, tick methods under a Ruby file to send only those; "Download .md" / "Copy .md" bundles everything into one attachment',
196
+ 'Paste & Place: right-click inside a Ruby file (or F1) → "Paste & Place Ruby Method...", paste the reply, "Find placement", check the diff, "Apply to editor" — then save; Ctrl+Z undoes',
197
+ 'Upload compare: drop or upload a file over an existing one and click "Compare" to see the diff before choosing Skip / Keep both / Overwrite'
198
+ ]
199
+ }
200
+ )
185
201
  )
186
202
  )
187
203
  )
@@ -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,
@@ -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 {
@@ -4135,3 +4139,28 @@ input.ide-model-graph-search[type="search"] {
4135
4139
  .mbeditor-collab-diag-label { font-weight: 500; }
4136
4140
  .mbeditor-collab-diag-detail { color: var(--ide-text-muted, #858585); margin-top: 3px; line-height: 1.5; }
4137
4141
  .mbeditor-collab-diag-foot { color: var(--ide-text-muted, #858585); margin-bottom: 12px; }
4142
+
4143
+ .combined-view { height: 100%; overflow-y: auto; background: var(--ide-bg, #1e1e1e); color: var(--ide-text, #d4d4d4); }
4144
+ .combined-note { padding: 8px 16px; font-size: 12px; color: var(--ide-text-muted, #888); }
4145
+ .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; }
4146
+ .combined-file-path { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
4147
+ .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; }
4148
+ .combined-file-btn:disabled { opacity: 0.4; cursor: default; }
4149
+ .combined-file-body { margin: 0; padding: 8px 16px 24px; overflow-x: auto; font: 12px/1.5 Menlo, Consolas, monospace; tab-size: 2; }
4150
+
4151
+ /* Combined view: method picking + Markdown export; import Compare; Paste & Place */
4152
+ .combined-toolbar { display: flex; gap: 8px; padding: 6px 16px; border-bottom: 1px solid var(--ide-border, #333); }
4153
+ .combined-methods { display: flex; flex-wrap: wrap; gap: 2px 12px; max-height: 96px; overflow-y: auto; padding: 4px 16px; font-size: 11px; border-bottom: 1px solid var(--ide-border, #333); }
4154
+ .combined-method { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; cursor: pointer; }
4155
+ .import-conflict-compare { margin-left: 8px; padding: 0; background: none; border: 0; color: var(--ide-accent, #0e639c); font-size: 11px; cursor: pointer; }
4156
+ .import-conflict-compare:hover { text-decoration: underline; }
4157
+ .import-conflict-modal.import-conflict-wide { min-width: min(900px, 94vw); max-width: 94vw; }
4158
+ .import-conflict-diff { height: 360px; margin-top: 8px; }
4159
+ .paste-place-modal { min-width: min(900px, 94vw); max-width: 94vw; }
4160
+ .paste-place-body { padding: 8px 16px; }
4161
+ .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; }
4162
+ .paste-place-error { color: var(--ide-error, #f48771); font-size: 12px; padding: 6px 0; }
4163
+ .paste-place-row { display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: 12px; }
4164
+ .paste-place-name { font-family: var(--ide-mono-font, monospace); min-width: 160px; }
4165
+ .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); }
4166
+ .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