mbeditor 0.13.1 → 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 +132 -0
- data/app/assets/javascripts/mbeditor/application.js +5 -0
- data/app/assets/javascripts/mbeditor/audit_log.js +165 -0
- data/app/assets/javascripts/mbeditor/collaboration_service.js +248 -26
- data/app/assets/javascripts/mbeditor/components/ChangelogView.js +89 -94
- data/app/assets/javascripts/mbeditor/components/CodeReviewPanel.js +6 -9
- data/app/assets/javascripts/mbeditor/components/CollapsibleSection.js +12 -7
- data/app/assets/javascripts/mbeditor/components/CombinedDiffViewer.js +20 -0
- data/app/assets/javascripts/mbeditor/components/CombinedFileView.js +214 -0
- data/app/assets/javascripts/mbeditor/components/DiffViewer.js +1 -1
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +442 -248
- data/app/assets/javascripts/mbeditor/components/FileHistoryPanel.js +6 -9
- data/app/assets/javascripts/mbeditor/components/FileTree.js +26 -12
- data/app/assets/javascripts/mbeditor/components/GitPanel.js +3 -0
- data/app/assets/javascripts/mbeditor/components/Gutter.js +51 -0
- data/app/assets/javascripts/mbeditor/components/ImportConflictModal.js +60 -5
- data/app/assets/javascripts/mbeditor/components/LogPanel.js +3 -44
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +1241 -1256
- data/app/assets/javascripts/mbeditor/components/ModelGraph.js +94 -37
- data/app/assets/javascripts/mbeditor/components/PastePlaceModal.js +329 -0
- data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +47 -65
- data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +26 -8
- data/app/assets/javascripts/mbeditor/components/SettingsModal.js +342 -0
- data/app/assets/javascripts/mbeditor/components/ShortcutHelp.js +18 -1
- data/app/assets/javascripts/mbeditor/components/TabBar.js +67 -98
- data/app/assets/javascripts/mbeditor/editor_plugins.js +705 -323
- data/app/assets/javascripts/mbeditor/file_import.js +13 -15
- data/app/assets/javascripts/mbeditor/file_service.js +48 -39
- data/app/assets/javascripts/mbeditor/git_service.js +15 -1
- data/app/assets/javascripts/mbeditor/history_service.js +5 -13
- data/app/assets/javascripts/mbeditor/search_service.js +9 -0
- data/app/assets/javascripts/mbeditor/tab_manager.js +127 -49
- data/app/assets/javascripts/mbeditor/websocket_service.js +13 -5
- data/app/assets/stylesheets/mbeditor/application.css +6 -1
- data/app/assets/stylesheets/mbeditor/editor.css +672 -249
- data/app/assets/stylesheets/mbeditor/glass.css +174 -0
- data/app/assets/stylesheets/mbeditor/themes.css +93 -33
- data/app/channels/mbeditor/collaboration_channel.rb +17 -4
- data/app/controllers/mbeditor/application_controller.rb +26 -3
- data/app/controllers/mbeditor/editors_controller.rb +124 -439
- data/app/services/mbeditor/archive_service.rb +137 -0
- data/app/services/mbeditor/collaboration_doc_store.rb +143 -14
- data/app/services/mbeditor/editor_state_service.rb +14 -51
- data/app/services/mbeditor/file_history_service.rb +222 -0
- data/app/services/mbeditor/git_info_service.rb +6 -0
- data/app/services/mbeditor/js_syntax_check_service.rb +42 -16
- data/app/services/mbeditor/lint_service.rb +137 -0
- data/app/services/mbeditor/locked_json_file.rb +67 -0
- data/app/services/mbeditor/process_runner.rb +32 -0
- data/app/services/mbeditor/ruby_lsp_result_translator.rb +226 -0
- data/app/services/mbeditor/ruby_method_ranges_service.rb +120 -0
- data/app/services/mbeditor/search_replace_service.rb +8 -0
- data/app/views/layouts/mbeditor/application.html.erb +1 -1
- data/lib/mbeditor/audit_log.rb +203 -0
- data/lib/mbeditor/configuration.rb +6 -1
- data/lib/mbeditor/rack/pending_migration_bypass.rb +31 -10
- data/lib/mbeditor/route_map.rb +5 -0
- data/lib/mbeditor/ruby_lsp_client.rb +82 -14
- data/lib/mbeditor/version.rb +1 -1
- data/lib/mbeditor.rb +1 -0
- 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 +15 -2
|
@@ -527,12 +527,8 @@ var ModelGraph = (function () {
|
|
|
527
527
|
var searchOpen = _searchOpen[0], setSearchOpen = _searchOpen[1];
|
|
528
528
|
var _highlight = React.useState(0);
|
|
529
529
|
var highlight = _highlight[0], setHighlight = _highlight[1];
|
|
530
|
-
var _focused = React.useState(null);
|
|
531
|
-
var focused = _focused[0], setFocused = _focused[1];
|
|
532
530
|
var _hovered = React.useState(null);
|
|
533
531
|
var hovered = _hovered[0], setHovered = _hovered[1];
|
|
534
|
-
var _pointer = React.useState({ x: 0, y: 0 });
|
|
535
|
-
var pointer = _pointer[0], setPointer = _pointer[1];
|
|
536
532
|
var dragRef = React.useRef(null);
|
|
537
533
|
var svgRef = React.useRef(null);
|
|
538
534
|
|
|
@@ -586,17 +582,56 @@ var ModelGraph = (function () {
|
|
|
586
582
|
if (sceneRef.current) sceneRef.current.classList.remove('mg-focus-mode');
|
|
587
583
|
}, []);
|
|
588
584
|
|
|
585
|
+
// The edge <g> elements touching each model, indexed once per scene
|
|
586
|
+
// instead of a querySelectorAll('[data-from=...],[data-to=...]') on every
|
|
587
|
+
// mouseenter — that scan walked the whole scene continuously as the
|
|
588
|
+
// pointer crossed a dense diagram. sceneChildrenRef is kept current just
|
|
589
|
+
// below sceneChildren's own memo, so a stale index is rebuilt exactly
|
|
590
|
+
// when the scene actually does.
|
|
591
|
+
var sceneChildrenRef = React.useRef(null);
|
|
592
|
+
var edgeIndexRef = React.useRef({ scene: null, map: {} });
|
|
593
|
+
var edgesFor = React.useCallback(function (name) {
|
|
594
|
+
var scene = sceneRef.current;
|
|
595
|
+
if (!scene) return [];
|
|
596
|
+
if (edgeIndexRef.current.scene !== sceneChildrenRef.current) {
|
|
597
|
+
var map = {};
|
|
598
|
+
Array.prototype.forEach.call(scene.querySelectorAll('[data-from]'), function (el) {
|
|
599
|
+
var from = el.getAttribute('data-from'), to = el.getAttribute('data-to');
|
|
600
|
+
(map[from] = map[from] || []).push(el);
|
|
601
|
+
if (to !== from) (map[to] = map[to] || []).push(el);
|
|
602
|
+
});
|
|
603
|
+
edgeIndexRef.current = { scene: sceneChildrenRef.current, map: map };
|
|
604
|
+
}
|
|
605
|
+
return edgeIndexRef.current.map[name] || [];
|
|
606
|
+
}, []);
|
|
607
|
+
|
|
608
|
+
// getBoundingClientRect forces a synchronous layout, and hovering has just
|
|
609
|
+
// written classes onto a <g> holding thousands of elements, so the layout
|
|
610
|
+
// it forces is the whole scene: measured at 10 ms a tick on a 420-model
|
|
611
|
+
// graph, against 0.9 ms when the style is clean. The pane's own rect moves
|
|
612
|
+
// only when the window or the panel layout does, so read it once a frame.
|
|
613
|
+
var rectRef = React.useRef(null);
|
|
614
|
+
var paneRect = React.useCallback(function () {
|
|
615
|
+
var el = svgRef.current;
|
|
616
|
+
if (!el) return null;
|
|
617
|
+
if (!rectRef.current) {
|
|
618
|
+
rectRef.current = el.getBoundingClientRect();
|
|
619
|
+
window.requestAnimationFrame(function () { rectRef.current = null; });
|
|
620
|
+
}
|
|
621
|
+
return rectRef.current;
|
|
622
|
+
}, []);
|
|
623
|
+
|
|
589
624
|
var moveHoverCard = React.useCallback(function (ev) {
|
|
590
625
|
var card = hoverCardRef.current;
|
|
591
626
|
var el = svgRef.current;
|
|
592
627
|
if (!card || !el) return;
|
|
593
|
-
var rect =
|
|
628
|
+
var rect = paneRect();
|
|
594
629
|
var x = ev.clientX - rect.left;
|
|
595
630
|
var y = ev.clientY - rect.top;
|
|
596
631
|
// Flip before the card would run off the pane rather than after.
|
|
597
632
|
card.style.left = (x > rect.width - 300 ? x - 290 : x + 18) + 'px';
|
|
598
633
|
card.style.top = Math.min(y + 18, Math.max(0, rect.height - 240)) + 'px';
|
|
599
|
-
}, []);
|
|
634
|
+
}, [paneRect]);
|
|
600
635
|
|
|
601
636
|
var enterModel = React.useCallback(function (name, ev) {
|
|
602
637
|
if (dragRef.current) return; // panning, not inspecting
|
|
@@ -604,19 +639,38 @@ var ModelGraph = (function () {
|
|
|
604
639
|
var scene = sceneRef.current;
|
|
605
640
|
if (scene) {
|
|
606
641
|
scene.classList.add('mg-focus-mode');
|
|
607
|
-
|
|
608
|
-
litRef.current = Array.prototype.slice.call(scene.querySelectorAll(sel));
|
|
642
|
+
litRef.current = edgesFor(name);
|
|
609
643
|
litRef.current.forEach(function (el) { el.classList.add('mg-lit'); });
|
|
610
644
|
}
|
|
611
645
|
setModelHover(name);
|
|
612
646
|
moveHoverCard(ev);
|
|
613
|
-
}, [clearLit, moveHoverCard]);
|
|
647
|
+
}, [clearLit, moveHoverCard, edgesFor]);
|
|
614
648
|
|
|
615
649
|
var leaveModel = React.useCallback(function () {
|
|
616
650
|
clearLit();
|
|
617
651
|
setModelHover(null);
|
|
618
652
|
}, [clearLit]);
|
|
619
653
|
|
|
654
|
+
// Position is written straight onto the tooltip's DOM node, the way
|
|
655
|
+
// moveHoverCard does for the model card — routing every mousemove over an
|
|
656
|
+
// edge through React state re-ran the whole component body per pointer
|
|
657
|
+
// tick. edgePosRef holds the latest point for the tooltip's own initial
|
|
658
|
+
// inline style (read once on the render its `hovered` turns true), without
|
|
659
|
+
// itself triggering a render.
|
|
660
|
+
var edgeTooltipRef = React.useRef(null);
|
|
661
|
+
var edgePosRef = React.useRef({ x: 0, y: 0 });
|
|
662
|
+
var moveEdgeTooltip = React.useCallback(function (ev) {
|
|
663
|
+
var rect = paneRect();
|
|
664
|
+
if (!rect) return;
|
|
665
|
+
var x = ev.clientX - rect.left;
|
|
666
|
+
var y = ev.clientY - rect.top;
|
|
667
|
+
edgePosRef.current = { x: x, y: y };
|
|
668
|
+
var card = edgeTooltipRef.current;
|
|
669
|
+
if (!card) return;
|
|
670
|
+
card.style.left = (x > rect.width - 260 ? x - 240 : x + 14) + 'px';
|
|
671
|
+
card.style.top = (y + 14) + 'px';
|
|
672
|
+
}, [paneRect]);
|
|
673
|
+
|
|
620
674
|
// Frame the whole graph on load. Without this the view starts at the
|
|
621
675
|
// top-left of a canvas much larger than the pane and the diagram looks
|
|
622
676
|
// empty until you go looking for it.
|
|
@@ -652,6 +706,19 @@ var ModelGraph = (function () {
|
|
|
652
706
|
|
|
653
707
|
React.useEffect(function () { fitToPane(); }, [fitToPane]);
|
|
654
708
|
|
|
709
|
+
// `focused` used to be React state read inside sceneChildren's className,
|
|
710
|
+
// but centring a model is also how a click and a search pick land, and
|
|
711
|
+
// that made every centre rebuild all ~3,500 SVG elements and their event
|
|
712
|
+
// closures. Applied to the mounted DOM instead, the same way
|
|
713
|
+
// enterModel/litRef apply the hover highlight above.
|
|
714
|
+
var focusedRef = React.useRef(null);
|
|
715
|
+
var applyFocus = React.useCallback(function (name) {
|
|
716
|
+
var scene = sceneRef.current;
|
|
717
|
+
if (focusedRef.current) focusedRef.current.classList.remove('mg-focused');
|
|
718
|
+
focusedRef.current = scene ? scene.querySelector('[data-model="' + name + '"]') : null;
|
|
719
|
+
if (focusedRef.current) focusedRef.current.classList.add('mg-focused');
|
|
720
|
+
}, []);
|
|
721
|
+
|
|
655
722
|
// Bring one model to the middle of the pane at the current zoom, and mark
|
|
656
723
|
// it so it's findable in a dense graph once it gets there.
|
|
657
724
|
var centreOn = React.useCallback(function (name) {
|
|
@@ -660,7 +727,7 @@ var ModelGraph = (function () {
|
|
|
660
727
|
if (!el || !pos) return;
|
|
661
728
|
var rect = el.getBoundingClientRect();
|
|
662
729
|
var h = nodeHeight(pos.model);
|
|
663
|
-
|
|
730
|
+
applyFocus(name);
|
|
664
731
|
setView(function (v) {
|
|
665
732
|
// Close half the distance to actual size, so a model found while
|
|
666
733
|
// fitted (often ~0.3) lands readable. Never zooms out: if you were
|
|
@@ -672,7 +739,7 @@ var ModelGraph = (function () {
|
|
|
672
739
|
y: rect.height / 2 - (pos.y + h / 2) * k
|
|
673
740
|
};
|
|
674
741
|
});
|
|
675
|
-
}, [placed]);
|
|
742
|
+
}, [placed, applyFocus]);
|
|
676
743
|
|
|
677
744
|
// Same mounting problem as the wheel listener: the fit effect can run
|
|
678
745
|
// before the SVG exists and not again afterwards. Kept current so the
|
|
@@ -707,22 +774,6 @@ var ModelGraph = (function () {
|
|
|
707
774
|
svgRef.current = el;
|
|
708
775
|
if (!el) return;
|
|
709
776
|
|
|
710
|
-
// getBoundingClientRect forces a synchronous layout, and we had just
|
|
711
|
-
// written a new transform onto a <g> holding thousands of elements — so
|
|
712
|
-
// reading it per wheel tick made the browser re-lay-out the entire scene
|
|
713
|
-
// before the handler could continue. Measured at 27 ms a tick on a
|
|
714
|
-
// 300-model graph. The pane's own rect only changes when the window or
|
|
715
|
-
// the panel layout does, so cache it and refresh once per frame.
|
|
716
|
-
var rectRef = { current: null };
|
|
717
|
-
var invalidateRect = function () { rectRef.current = null; };
|
|
718
|
-
var paneRect = function () {
|
|
719
|
-
if (!rectRef.current) {
|
|
720
|
-
rectRef.current = el.getBoundingClientRect();
|
|
721
|
-
window.requestAnimationFrame(invalidateRect);
|
|
722
|
-
}
|
|
723
|
-
return rectRef.current;
|
|
724
|
-
};
|
|
725
|
-
|
|
726
777
|
var onWheel = function (e) {
|
|
727
778
|
e.preventDefault();
|
|
728
779
|
var rect = paneRect();
|
|
@@ -739,7 +790,7 @@ var ModelGraph = (function () {
|
|
|
739
790
|
|
|
740
791
|
// After layout, so the pane has a measurable size to fit into.
|
|
741
792
|
window.requestAnimationFrame(function () { fitRef.current(); });
|
|
742
|
-
}, []);
|
|
793
|
+
}, [paneRect]);
|
|
743
794
|
|
|
744
795
|
// Memoised so hovering a model does not rebuild every box and edge.
|
|
745
796
|
// The highlight itself is applied by toggling DOM classes; only the
|
|
@@ -790,10 +841,7 @@ var ModelGraph = (function () {
|
|
|
790
841
|
if (line) line.classList.add('mg-edge-hovered');
|
|
791
842
|
setHovered({ index: i, edge: e });
|
|
792
843
|
},
|
|
793
|
-
onMouseMove:
|
|
794
|
-
var rect = svgRef.current.getBoundingClientRect();
|
|
795
|
-
setPointer({ x: ev.clientX - rect.left, y: ev.clientY - rect.top });
|
|
796
|
-
},
|
|
844
|
+
onMouseMove: moveEdgeTooltip,
|
|
797
845
|
onMouseLeave: function (ev) {
|
|
798
846
|
var line = ev.currentTarget.nextElementSibling;
|
|
799
847
|
if (line) line.classList.remove('mg-edge-hovered');
|
|
@@ -818,7 +866,8 @@ var ModelGraph = (function () {
|
|
|
818
866
|
'g',
|
|
819
867
|
{
|
|
820
868
|
key: m.name,
|
|
821
|
-
|
|
869
|
+
'data-model': m.name,
|
|
870
|
+
className: 'mg-node',
|
|
822
871
|
transform: 'translate(' + pos.x + ',' + pos.y + ')',
|
|
823
872
|
onMouseEnter: function (ev) { enterModel(m.name, ev); },
|
|
824
873
|
onMouseMove: function (ev) { moveHoverCard(ev); },
|
|
@@ -887,7 +936,12 @@ var ModelGraph = (function () {
|
|
|
887
936
|
);
|
|
888
937
|
})
|
|
889
938
|
];
|
|
890
|
-
}, [placed, graph
|
|
939
|
+
}, [placed, graph]);
|
|
940
|
+
// Kept current so edgesFor (declared earlier, used from enterModel) can
|
|
941
|
+
// detect a scene rebuild without itself listing sceneChildren as a
|
|
942
|
+
// dependency — that would rebuild enterModel, and everything closing
|
|
943
|
+
// over it, on every scene rebuild too.
|
|
944
|
+
sceneChildrenRef.current = sceneChildren;
|
|
891
945
|
|
|
892
946
|
if (loading) {
|
|
893
947
|
return React.createElement('div', { className: 'ide-model-graph-empty' }, 'Building the model graph…');
|
|
@@ -1058,12 +1112,15 @@ var ModelGraph = (function () {
|
|
|
1058
1112
|
hovered && React.createElement(
|
|
1059
1113
|
'div',
|
|
1060
1114
|
{
|
|
1115
|
+
ref: edgeTooltipRef,
|
|
1061
1116
|
className: 'mg-tooltip',
|
|
1062
1117
|
// Offset from the cursor, and flipped left near the right edge so
|
|
1063
|
-
// the tooltip never runs off the pane.
|
|
1118
|
+
// the tooltip never runs off the pane. Only the initial placement is
|
|
1119
|
+
// computed here — moveEdgeTooltip writes the DOM node directly as
|
|
1120
|
+
// the pointer moves, so this style is live for one frame at most.
|
|
1064
1121
|
style: {
|
|
1065
|
-
left:
|
|
1066
|
-
top: (
|
|
1122
|
+
left: edgePosRef.current.x + (svgRef.current && edgePosRef.current.x > svgRef.current.clientWidth - 260 ? -240 : 14) + 'px',
|
|
1123
|
+
top: (edgePosRef.current.y + 14) + 'px'
|
|
1067
1124
|
}
|
|
1068
1125
|
},
|
|
1069
1126
|
React.createElement('div', { className: 'mg-tooltip-title' },
|
|
@@ -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;
|