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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +64 -0
- data/app/assets/javascripts/mbeditor/application.js +2 -0
- data/app/assets/javascripts/mbeditor/components/CombinedFileView.js +228 -0
- data/app/assets/javascripts/mbeditor/components/DiffViewer.js +2 -1
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +18 -1
- data/app/assets/javascripts/mbeditor/components/ImportConflictModal.js +60 -5
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +98 -9
- 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/javascripts/mbeditor/tab_manager.js +3 -0
- data/app/assets/stylesheets/mbeditor/editor.css +33 -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
|
@@ -1453,7 +1453,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1453
1453
|
// restore one.
|
|
1454
1454
|
panesToLoad = panesToLoad.map(function (p) {
|
|
1455
1455
|
return _extends({}, p, {
|
|
1456
|
-
tabs: p.tabs.filter(function (t) { return !t.isSettings && t.path !== '__settings__'; })
|
|
1456
|
+
tabs: p.tabs.filter(function (t) { return !t.isSettings && t.path !== '__settings__' && !t.isCombinedView; })
|
|
1457
1457
|
});
|
|
1458
1458
|
});
|
|
1459
1459
|
var allTabs = panesToLoad.flatMap(function (p) { return p.tabs; });
|
|
@@ -1492,7 +1492,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1492
1492
|
p.tabs.forEach(function (t) {
|
|
1493
1493
|
var res = results[resIdx++];
|
|
1494
1494
|
var isPlainFile = t.path && !t.isDiff && !t.isCombinedDiff &&
|
|
1495
|
-
!t.isChangelog && !t.isPreview &&
|
|
1495
|
+
!t.isChangelog && !t.isCombinedView && !t.isPreview &&
|
|
1496
1496
|
!/^(diff|combined-diff):\/\//.test(t.path) && !/::preview$/.test(t.path);
|
|
1497
1497
|
if (isPlainFile) {
|
|
1498
1498
|
if (seenPaths[t.path]) return;
|
|
@@ -1613,7 +1613,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1613
1613
|
return {
|
|
1614
1614
|
id: p.id,
|
|
1615
1615
|
activeTabId: p.activeTabId,
|
|
1616
|
-
tabs: p.tabs.filter(function (t) { return !t.isCombinedDiff && !t.isUntitled; }).map(function (t) {
|
|
1616
|
+
tabs: p.tabs.filter(function (t) { return !t.isCombinedDiff && !t.isCombinedView && !t.isUntitled; }).map(function (t) {
|
|
1617
1617
|
return {
|
|
1618
1618
|
id: t.id, path: t.path, name: t.name, dirty: t.dirty, viewState: t.viewState,
|
|
1619
1619
|
isPreview: !!t.isPreview, previewFor: t.previewFor || null,
|
|
@@ -2149,6 +2149,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2149
2149
|
!path.startsWith('combined-diff://') &&
|
|
2150
2150
|
!path.startsWith('untitled://') &&
|
|
2151
2151
|
!pt.tab.isCombinedDiff &&
|
|
2152
|
+
!pt.tab.isCombinedView &&
|
|
2152
2153
|
!pt.tab.isImage &&
|
|
2153
2154
|
!pt.tab.isDiff &&
|
|
2154
2155
|
typeof pt.tab.content === 'string';
|
|
@@ -2564,7 +2565,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2564
2565
|
return {
|
|
2565
2566
|
id: p.id,
|
|
2566
2567
|
activeTabId: p.activeTabId,
|
|
2567
|
-
tabs: p.tabs.filter(function(t) { return !t.isCombinedDiff && !t.isUntitled; }).map(function (t) {
|
|
2568
|
+
tabs: p.tabs.filter(function(t) { return !t.isCombinedDiff && !t.isCombinedView && !t.isUntitled; }).map(function (t) {
|
|
2568
2569
|
return {
|
|
2569
2570
|
id: t.id,
|
|
2570
2571
|
path: t.path,
|
|
@@ -2803,7 +2804,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2803
2804
|
var _presenceFileFor = function (tab) {
|
|
2804
2805
|
if (!tab || !tab.path) return null;
|
|
2805
2806
|
var p = tab.path;
|
|
2806
|
-
if (tab.isDiff || tab.isCombinedDiff || tab.isCommitGraph || tab.isPreview || tab.isChangelog) return null;
|
|
2807
|
+
if (tab.isDiff || tab.isCombinedDiff || tab.isCommitGraph || tab.isPreview || tab.isChangelog || tab.isCombinedView) return null;
|
|
2807
2808
|
if (p.indexOf('diff://') === 0 || p.indexOf('combined-diff://') === 0 || p.indexOf('mbeditor://') === 0) return null;
|
|
2808
2809
|
if (p.indexOf('::preview') !== -1) return null;
|
|
2809
2810
|
return p;
|
|
@@ -3129,7 +3130,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3129
3130
|
var setActiveEOL = _useState31e[1];
|
|
3130
3131
|
|
|
3131
3132
|
useEffect(function () {
|
|
3132
|
-
if (!gitAvailable || !activeTab || activeTab.isDiff || activeTab.isCombinedDiff || activeTab.isCommitGraph || !activeTab.path || activeTab.path.indexOf('://') >= 0) {
|
|
3133
|
+
if (!gitAvailable || !activeTab || activeTab.isDiff || activeTab.isCombinedDiff || activeTab.isCombinedView || activeTab.isCommitGraph || !activeTab.path || activeTab.path.indexOf('://') >= 0) {
|
|
3133
3134
|
setActiveFileCommit(null);
|
|
3134
3135
|
return;
|
|
3135
3136
|
}
|
|
@@ -3151,7 +3152,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3151
3152
|
// buffer for '\r' there was pure waste — typing cannot change the line ending.
|
|
3152
3153
|
useEffect(function () {
|
|
3153
3154
|
if (!activeTab || typeof activeTab.content !== 'string' ||
|
|
3154
|
-
activeTab.isDiff || activeTab.isCombinedDiff || activeTab.isCommitGraph || activeTab.isPreview) {
|
|
3155
|
+
activeTab.isDiff || activeTab.isCombinedDiff || activeTab.isCombinedView || activeTab.isCommitGraph || activeTab.isPreview) {
|
|
3155
3156
|
setActiveEOL(null);
|
|
3156
3157
|
return;
|
|
3157
3158
|
}
|
|
@@ -3166,7 +3167,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3166
3167
|
|
|
3167
3168
|
useEffect(function () {
|
|
3168
3169
|
if (!activeTab || typeof activeTab.content !== 'string') return;
|
|
3169
|
-
if (activeTab.isDiff || activeTab.isCombinedDiff || activeTab.isCommitGraph) return;
|
|
3170
|
+
if (activeTab.isDiff || activeTab.isCombinedDiff || activeTab.isCombinedView || activeTab.isCommitGraph) return;
|
|
3170
3171
|
if (isRubyPath(activeTab.path) && !rubocopAvailable) return;
|
|
3171
3172
|
if (activeTab.path.endsWith('.haml') && !hamlLintAvailable) return;
|
|
3172
3173
|
|
|
@@ -4289,6 +4290,57 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4289
4290
|
return !!(node && selectedPaths && selectedPaths.size > 1 && selectedPaths.has(node.path));
|
|
4290
4291
|
};
|
|
4291
4292
|
|
|
4293
|
+
var MAX_COMBINED_FILES = 200;
|
|
4294
|
+
var handleOpenCombinedView = function handleOpenCombinedView(node) {
|
|
4295
|
+
if (!node) return;
|
|
4296
|
+
var byPath = {};
|
|
4297
|
+
(function index(items) {
|
|
4298
|
+
(items || []).forEach(function (n) { byPath[n.path] = n; index(n.children); });
|
|
4299
|
+
})(treeData);
|
|
4300
|
+
var files = [];
|
|
4301
|
+
var seen = {};
|
|
4302
|
+
var walk = function walk(n) {
|
|
4303
|
+
if (!n) return;
|
|
4304
|
+
if (n.type === 'file') {
|
|
4305
|
+
if (!seen[n.path]) { seen[n.path] = true; files.push(n.path); }
|
|
4306
|
+
return;
|
|
4307
|
+
}
|
|
4308
|
+
(n.children || []).forEach(walk);
|
|
4309
|
+
};
|
|
4310
|
+
(actsOnSelection(node) ? Array.from(selectedPaths) : [node.path]).forEach(function (p) { walk(byPath[p]); });
|
|
4311
|
+
openCombinedTab(files);
|
|
4312
|
+
};
|
|
4313
|
+
|
|
4314
|
+
var openCombinedTab = function openCombinedTab(files) {
|
|
4315
|
+
if (!files.length) { EditorStore.setStatus('No files to show', 'info'); return; }
|
|
4316
|
+
var total = files.length;
|
|
4317
|
+
var id = 'mbeditor://combined/' + Date.now();
|
|
4318
|
+
var st = EditorStore.getState();
|
|
4319
|
+
var pane = st.panes.find(function (p) { return p.id === st.focusedPaneId; }) || st.panes[0];
|
|
4320
|
+
if (!pane) return;
|
|
4321
|
+
var tab = { id: id, path: id, name: 'Combined (' + Math.min(total, MAX_COMBINED_FILES) + ' files)', dirty: false, content: '', isCombinedView: true, combinedPaths: files.slice(0, MAX_COMBINED_FILES), combinedTotal: total };
|
|
4322
|
+
EditorStore.setState({
|
|
4323
|
+
panes: st.panes.map(function (p) { return p.id === pane.id ? Object.assign({}, p, { tabs: p.tabs.concat(tab), activeTabId: id }) : p; }),
|
|
4324
|
+
focusedPaneId: pane.id
|
|
4325
|
+
});
|
|
4326
|
+
};
|
|
4327
|
+
|
|
4328
|
+
// Every plain file tab across both panes, in tab order. Virtual tabs (diffs,
|
|
4329
|
+
// previews, settings, other combined views) carry a scheme or a flag.
|
|
4330
|
+
var handleOpenEditorsCombined = function handleOpenEditorsCombined() {
|
|
4331
|
+
var seen = {};
|
|
4332
|
+
var files = [];
|
|
4333
|
+
EditorStore.getState().panes.forEach(function (p) {
|
|
4334
|
+
p.tabs.forEach(function (t) {
|
|
4335
|
+
if (!t.path || t.isSettings || t.isUntitled || t.isImage || t.isDiff || t.isCombinedDiff || t.isCombinedView || t.isChangelog || t.isPreview || t.isCommitGraph) return;
|
|
4336
|
+
if (t.path.indexOf('://') !== -1 || /::preview$/.test(t.path) || seen[t.path]) return;
|
|
4337
|
+
seen[t.path] = true;
|
|
4338
|
+
files.push(t.path);
|
|
4339
|
+
});
|
|
4340
|
+
});
|
|
4341
|
+
openCombinedTab(files);
|
|
4342
|
+
};
|
|
4343
|
+
|
|
4292
4344
|
var handleDownloadFile = function handleDownloadFile(node) {
|
|
4293
4345
|
if (!node) return;
|
|
4294
4346
|
var isMultiSelect = actsOnSelection(node);
|
|
@@ -4393,6 +4445,9 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4393
4445
|
if (action === 'delete') {
|
|
4394
4446
|
handleDeletePath(node);return;
|
|
4395
4447
|
}
|
|
4448
|
+
if (action === 'combinedView' && node) {
|
|
4449
|
+
handleOpenCombinedView(node);return;
|
|
4450
|
+
}
|
|
4396
4451
|
if (action === 'download' && node) {
|
|
4397
4452
|
handleDownloadFile(node);return;
|
|
4398
4453
|
}
|
|
@@ -5428,6 +5483,12 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
5428
5483
|
actions: React.createElement(
|
|
5429
5484
|
SectionActionGroup,
|
|
5430
5485
|
{ ariaLabel: "Open editor actions" },
|
|
5486
|
+
React.createElement(SidebarActionButton, {
|
|
5487
|
+
title: "Open all in Combined View",
|
|
5488
|
+
ariaLabel: "Open all open editors in Combined View",
|
|
5489
|
+
iconClass: "fas fa-layer-group",
|
|
5490
|
+
onClick: handleOpenEditorsCombined
|
|
5491
|
+
}),
|
|
5431
5492
|
React.createElement(SidebarActionButton, {
|
|
5432
5493
|
title: "Close all editors",
|
|
5433
5494
|
ariaLabel: "Close all open editors",
|
|
@@ -6113,6 +6174,12 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
6113
6174
|
}
|
|
6114
6175
|
}
|
|
6115
6176
|
});
|
|
6177
|
+
} else if (pActiveTab.isCombinedView) {
|
|
6178
|
+
content = React.createElement(CombinedFileView, {
|
|
6179
|
+
key: pActiveTab.id,
|
|
6180
|
+
paths: pActiveTab.combinedPaths || [],
|
|
6181
|
+
total: pActiveTab.combinedTotal || 0
|
|
6182
|
+
});
|
|
6116
6183
|
} else if (pActiveTab.isDiff) {
|
|
6117
6184
|
var _t = editorPrefs.theme || 'vs-dark';
|
|
6118
6185
|
var isDiffDark = _t !== 'vs' && _t !== 'hc-light' && _t !== 'github-light';
|
|
@@ -6173,7 +6240,10 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
6173
6240
|
{
|
|
6174
6241
|
className: "ide-pane " + (isFocused ? 'focused' : '') + " " + (isDropTarget ? 'drop-target' : ''),
|
|
6175
6242
|
style: { flexBasis: flexBasis, flexShrink: 0, flexGrow: 0, display: 'flex', flexDirection: 'column', minWidth: 0 },
|
|
6176
|
-
|
|
6243
|
+
// Mousedown, not click: focusing re-renders the app, and doing that
|
|
6244
|
+
// during a click's capture phase reverts a checkbox before React
|
|
6245
|
+
// sees it change, so its onChange never fires.
|
|
6246
|
+
onMouseDownCapture: function (e) {
|
|
6177
6247
|
return TabManager.focusPane(pane.id);
|
|
6178
6248
|
},
|
|
6179
6249
|
onDragOver: function (e) {
|
|
@@ -6756,6 +6826,14 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
6756
6826
|
" " + deleteMenuLabel(contextMenu.node)
|
|
6757
6827
|
),
|
|
6758
6828
|
React.createElement("div", { className: "context-menu-divider" }),
|
|
6829
|
+
contextMenu.node && (actsOnSelection(contextMenu.node) || contextMenu.node.type === 'folder') && React.createElement(
|
|
6830
|
+
"div",
|
|
6831
|
+
{ className: "context-menu-item", onClick: function () {
|
|
6832
|
+
return handleContextMenuAction('combinedView');
|
|
6833
|
+
} },
|
|
6834
|
+
React.createElement("i", { className: "fas fa-layer-group context-menu-icon" }),
|
|
6835
|
+
" Open in Combined View"
|
|
6836
|
+
),
|
|
6759
6837
|
contextMenu.node && React.createElement(
|
|
6760
6838
|
"div",
|
|
6761
6839
|
{ className: "context-menu-item", onClick: function () {
|
|
@@ -6860,9 +6938,20 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
6860
6938
|
importConflict && React.createElement(ImportConflictModal, {
|
|
6861
6939
|
conflicts: importConflict.result.conflicts,
|
|
6862
6940
|
errors: importConflict.result.errors,
|
|
6941
|
+
entries: importConflict.entries,
|
|
6942
|
+
targetFolderPath: importConflict.targetFolderPath,
|
|
6943
|
+
isDark: !['vs', 'hc-light', 'github-light'].includes(editorPrefs.theme || 'vs-dark'),
|
|
6944
|
+
editorPrefs: editorPrefs,
|
|
6863
6945
|
onResolve: resolveImportConflict
|
|
6864
6946
|
}),
|
|
6865
6947
|
|
|
6948
|
+
/* ── Paste & Place ─────────────────────────────────────────────────── */
|
|
6949
|
+
state.isPastePlaceVisible && React.createElement(PastePlaceModal, {
|
|
6950
|
+
isDark: !['vs', 'hc-light', 'github-light'].includes(editorPrefs.theme || 'vs-dark'),
|
|
6951
|
+
editorPrefs: editorPrefs,
|
|
6952
|
+
onClose: function () { EditorStore.setState({ isPastePlaceVisible: false }); }
|
|
6953
|
+
}),
|
|
6954
|
+
|
|
6866
6955
|
/* ── Schema modal ──────────────────────────────────────────────────── */
|
|
6867
6956
|
schemaModal && React.createElement(
|
|
6868
6957
|
'div',
|
|
@@ -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. The stacked-layers icon on OPEN EDITORS does the same for all open files',
|
|
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
|
)
|