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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -0
- data/app/assets/javascripts/mbeditor/application.js +2 -0
- data/app/assets/javascripts/mbeditor/components/CombinedFileView.js +214 -0
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +13 -1
- data/app/assets/javascripts/mbeditor/components/ImportConflictModal.js +60 -5
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +68 -8
- data/app/assets/javascripts/mbeditor/components/PastePlaceModal.js +329 -0
- data/app/assets/javascripts/mbeditor/components/ShortcutHelp.js +16 -0
- data/app/assets/javascripts/mbeditor/editor_plugins.js +20 -20
- data/app/assets/javascripts/mbeditor/file_service.js +10 -0
- data/app/assets/stylesheets/mbeditor/editor.css +30 -1
- data/app/assets/stylesheets/mbeditor/glass.css +11 -0
- data/app/assets/stylesheets/mbeditor/themes.css +3 -3
- data/app/controllers/mbeditor/editors_controller.rb +7 -0
- data/app/services/mbeditor/ruby_method_ranges_service.rb +120 -0
- data/lib/mbeditor/rack/pending_migration_bypass.rb +16 -1
- data/lib/mbeditor/route_map.rb +1 -0
- data/lib/mbeditor/version.rb +1 -1
- data/public/monaco-editor/VERSIONS +1 -1
- data/public/monaco-editor/codicon.ttf +0 -0
- data/public/monaco-editor/css.worker.js +41 -37
- data/public/monaco-editor/editor.worker.js +19 -15
- data/public/monaco-editor/html.worker.js +53 -49
- data/public/monaco-editor/json.worker.js +36 -32
- data/public/monaco-editor/monaco.css +1 -1
- data/public/monaco-editor/monaco.js +388 -301
- data/public/monaco-editor/ts.worker.js +226 -222
- data/vendor/assets/javascripts/axios.min.js +3 -3
- 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1257
|
-
monaco.
|
|
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.
|
|
1275
|
-
target: monaco.
|
|
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.
|
|
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.
|
|
1293
|
-
Object.assign({}, monaco.
|
|
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.
|
|
1405
|
-
monaco.
|
|
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.
|
|
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.
|
|
1729
|
-
monaco.
|
|
1730
|
-
target: monaco.
|
|
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.
|
|
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.
|
|
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-
|
|
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: #
|
|
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: #
|
|
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: #
|
|
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
|