mbeditor 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +157 -1
  3. data/app/assets/javascripts/mbeditor/application.js +3 -1
  4. data/app/assets/javascripts/mbeditor/audit_log.js +165 -0
  5. data/app/assets/javascripts/mbeditor/collaboration_service.js +264 -22
  6. data/app/assets/javascripts/mbeditor/components/ChangelogView.js +89 -94
  7. data/app/assets/javascripts/mbeditor/components/CodeReviewPanel.js +6 -9
  8. data/app/assets/javascripts/mbeditor/components/CollapsibleSection.js +12 -7
  9. data/app/assets/javascripts/mbeditor/components/CombinedDiffViewer.js +20 -0
  10. data/app/assets/javascripts/mbeditor/components/DiffViewer.js +1 -1
  11. data/app/assets/javascripts/mbeditor/components/EditorPanel.js +440 -334
  12. data/app/assets/javascripts/mbeditor/components/FileHistoryPanel.js +6 -9
  13. data/app/assets/javascripts/mbeditor/components/FileTree.js +26 -12
  14. data/app/assets/javascripts/mbeditor/components/GitPanel.js +3 -0
  15. data/app/assets/javascripts/mbeditor/components/Gutter.js +51 -0
  16. data/app/assets/javascripts/mbeditor/components/ImportDialog.js +9 -1
  17. data/app/assets/javascripts/mbeditor/components/LogPanel.js +3 -44
  18. data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +1322 -1431
  19. data/app/assets/javascripts/mbeditor/components/ModelGraph.js +94 -37
  20. data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +82 -72
  21. data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +121 -81
  22. data/app/assets/javascripts/mbeditor/components/SettingsModal.js +342 -0
  23. data/app/assets/javascripts/mbeditor/components/ShortcutHelp.js +2 -1
  24. data/app/assets/javascripts/mbeditor/components/TabBar.js +67 -98
  25. data/app/assets/javascripts/mbeditor/editor_plugins.js +694 -306
  26. data/app/assets/javascripts/mbeditor/file_import.js +13 -15
  27. data/app/assets/javascripts/mbeditor/file_service.js +46 -57
  28. data/app/assets/javascripts/mbeditor/git_service.js +15 -1
  29. data/app/assets/javascripts/mbeditor/history_service.js +5 -13
  30. data/app/assets/javascripts/mbeditor/search_service.js +29 -2
  31. data/app/assets/javascripts/mbeditor/tab_manager.js +135 -54
  32. data/app/assets/javascripts/mbeditor/websocket_service.js +13 -5
  33. data/app/assets/stylesheets/mbeditor/application.css +6 -1
  34. data/app/assets/stylesheets/mbeditor/editor.css +762 -297
  35. data/app/assets/stylesheets/mbeditor/glass.css +163 -0
  36. data/app/assets/stylesheets/mbeditor/themes.css +90 -30
  37. data/app/channels/mbeditor/collaboration_channel.rb +25 -6
  38. data/app/controllers/mbeditor/application_controller.rb +26 -3
  39. data/app/controllers/mbeditor/editors_controller.rb +125 -465
  40. data/app/services/mbeditor/archive_service.rb +137 -0
  41. data/app/services/mbeditor/collaboration_doc_store.rb +180 -12
  42. data/app/services/mbeditor/duplicate_content_scanner.rb +105 -0
  43. data/app/services/mbeditor/editor_state_service.rb +14 -51
  44. data/app/services/mbeditor/file_history_service.rb +222 -0
  45. data/app/services/mbeditor/git_info_service.rb +6 -0
  46. data/app/services/mbeditor/js_globals_service.rb +12 -1
  47. data/app/services/mbeditor/js_syntax_check_service.rb +42 -16
  48. data/app/services/mbeditor/lint_service.rb +137 -0
  49. data/app/services/mbeditor/locked_json_file.rb +67 -0
  50. data/app/services/mbeditor/process_runner.rb +32 -0
  51. data/app/services/mbeditor/rubocop_run_service.rb +17 -5
  52. data/app/services/mbeditor/ruby_lsp_result_translator.rb +226 -0
  53. data/app/services/mbeditor/schema_service.rb +8 -2
  54. data/app/services/mbeditor/search_replace_service.rb +19 -2
  55. data/app/services/mbeditor/test_runner_service.rb +3 -56
  56. data/app/views/layouts/mbeditor/application.html.erb +1 -1
  57. data/lib/mbeditor/audit_log.rb +203 -0
  58. data/lib/mbeditor/configuration.rb +6 -9
  59. data/lib/mbeditor/rack/pending_migration_bypass.rb +15 -9
  60. data/lib/mbeditor/route_map.rb +4 -1
  61. data/lib/mbeditor/ruby_lsp_client.rb +82 -14
  62. data/lib/mbeditor/version.rb +1 -1
  63. data/lib/mbeditor.rb +1 -0
  64. data/lib/tasks/mbeditor.rake +23 -0
  65. metadata +14 -3
  66. data/app/assets/javascripts/mbeditor/components/TestRunPanel.js +0 -312
@@ -6,19 +6,16 @@ var FileHistoryPanel = function FileHistoryPanel(_ref) {
6
6
  var onClose = _ref.onClose;
7
7
 
8
8
  var _React$useState = React.useState([]),
9
- _React$useState2 = _slicedToArray(_React$useState, 2),
10
- commits = _React$useState2[0],
11
- setCommits = _React$useState2[1];
9
+ commits = _React$useState[0],
10
+ setCommits = _React$useState[1];
12
11
 
13
12
  var _React$useState3 = React.useState(true),
14
- _React$useState4 = _slicedToArray(_React$useState3, 2),
15
- loading = _React$useState4[0],
16
- setLoading = _React$useState4[1];
13
+ loading = _React$useState3[0],
14
+ setLoading = _React$useState3[1];
17
15
 
18
16
  var _React$useState5 = React.useState(null),
19
- _React$useState6 = _slicedToArray(_React$useState5, 2),
20
- error = _React$useState6[0],
21
- setError = _React$useState6[1];
17
+ error = _React$useState5[0],
18
+ setError = _React$useState5[1];
22
19
 
23
20
  React.useEffect(function () {
24
21
  if (!path) return;
@@ -1,6 +1,5 @@
1
1
  'use strict';
2
2
 
3
- var _slicedToArray = (function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i['return']) _i['return'](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError('Invalid attempt to destructure non-iterable instance'); } }; })();
4
3
 
5
4
  function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } else { return Array.from(arr); } }
6
5
 
@@ -25,6 +24,7 @@ var FileTree = function FileTree(_ref) {
25
24
  var activePath = _ref.activePath;
26
25
  var selectedPaths = _ref.selectedPaths; // Set<string> — all selected paths
27
26
  var anchorPath = _ref.anchorPath; // string — anchor for shift-click range
27
+ var revealTarget = _ref.revealTarget; // { path } — one-shot scroll target after create/rename/delete
28
28
  var onNodeSelect = _ref.onNodeSelect; // fn(node) — single select (also clears multi)
29
29
  var onMultiSelect = _ref.onMultiSelect; // fn(Set<string>) — multi-select update
30
30
  var onMove = _ref.onMove; // fn(srcPaths[], destFolderPath) — DnD move
@@ -44,23 +44,20 @@ var FileTree = function FileTree(_ref) {
44
44
 
45
45
  var _useState = useState('');
46
46
 
47
- var _useState2 = _slicedToArray(_useState, 2);
48
47
 
49
- var inlineValue = _useState2[0];
50
- var setInlineValue = _useState2[1];
48
+ var inlineValue = _useState[0];
49
+ var setInlineValue = _useState[1];
51
50
 
52
51
  var _useStateDnD = useState(null);
53
- var _useStateDnD2 = _slicedToArray(_useStateDnD, 2);
54
- var dragOverFolder = _useStateDnD2[0];
55
- var setDragOverFolder = _useStateDnD2[1];
52
+ var dragOverFolder = _useStateDnD[0];
53
+ var setDragOverFolder = _useStateDnD[1];
56
54
 
57
55
  // Target of an in-flight *external* drag: a folder path, '' for the tree
58
56
  // root, or null when no external drag is over the tree. Kept separate from
59
57
  // dragOverFolder so the two drop kinds highlight differently.
60
58
  var _useStateExt = useState(null);
61
- var _useStateExt2 = _slicedToArray(_useStateExt, 2);
62
- var externalDragOver = _useStateExt2[0];
63
- var setExternalDragOver = _useStateExt2[1];
59
+ var externalDragOver = _useStateExt[0];
60
+ var setExternalDragOver = _useStateExt[1];
64
61
 
65
62
  var inlineRef = useRef(null);
66
63
  var committedRef = useRef(false);
@@ -123,6 +120,18 @@ var FileTree = function FileTree(_ref) {
123
120
  return function () { clearTimeout(timer); };
124
121
  }, [anchorPath]);
125
122
 
123
+ // Scroll the affected row into view after a create/rename/delete. The
124
+ // caller expands any ancestor dirs before setting revealTarget, so this
125
+ // only has to wait for the tree re-render and locate the row.
126
+ useEffect(function () {
127
+ if (!revealTarget || !revealTarget.path) return;
128
+ var timer = setTimeout(function () {
129
+ var idx = flatItemsRef.current.findIndex(function(item) { return item.kind === 'node' && item.node.path === revealTarget.path; });
130
+ if (idx >= 0) scrollToItem(idx);
131
+ }, 80);
132
+ return function () { clearTimeout(timer); };
133
+ }, [revealTarget]);
134
+
126
135
  // Auto-reveal: when activePath changes, expand all ancestor dirs and scroll into view
127
136
  useEffect(function () {
128
137
  if (!activePath) return;
@@ -651,7 +660,8 @@ var FileTree = function FileTree(_ref) {
651
660
  onContextMenu: function(e) {
652
661
  e.preventDefault();
653
662
  e.stopPropagation();
654
- selectNode(node);
663
+ var isPartOfMultiSelect = selectedPaths && selectedPaths.size > 1 && selectedPaths.has(node.path);
664
+ if (!isPartOfMultiSelect) selectNode(node);
655
665
  if (onContextMenu) onContextMenu(e, node);
656
666
  }
657
667
  },
@@ -715,7 +725,11 @@ var FileTreeMemo = React.memo(FileTree, function(prev, next) {
715
725
  prev.gitFiles === next.gitFiles &&
716
726
  prev.expandedDirs === next.expandedDirs &&
717
727
  prev.pendingCreate === next.pendingCreate &&
718
- prev.pendingRename === next.pendingRename;
728
+ prev.pendingRename === next.pendingRename &&
729
+ // A fresh object per reveal request, so identity is the signal. Without it
730
+ // the reveal only lands when the tree happened to change in the same batch,
731
+ // which is most of the time and never all of it.
732
+ prev.revealTarget === next.revealTarget;
719
733
  });
720
734
 
721
735
  // Expose globally for sprockets require
@@ -84,6 +84,9 @@ var GitPanel = function GitPanel(_ref) {
84
84
  var statusMeta = function statusMeta(rawStatus) {
85
85
  var raw = (rawStatus || '').trim();
86
86
  if (raw === '??') return { badge: 'NEW', cssKey: 'A', description: 'Untracked' };
87
+ // Before the first-character switch below: AA and DD are conflicts, and it
88
+ // would call them "added" and "deleted".
89
+ if (window.MBEDITOR_UNMERGED_STATUSES[raw]) return { badge: 'U', cssKey: 'Q', description: 'Conflict' };
87
90
  if (raw.startsWith('R')) return { badge: 'R', cssKey: 'R', description: 'Renamed' };
88
91
  switch (raw.charAt(0)) {
89
92
  case 'M': return { badge: 'M', cssKey: 'M', description: 'Modified' };
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ // Gutter — the one draggable strip between two cards.
4
+ //
5
+ // It lives in the gap, never inside the panel it resizes: a grip rendered
6
+ // inside a card sits on the card's own background and reads as decoration
7
+ // rather than as the seam you can pull. Every gutter in the app is this
8
+ // component; the panel next to it owns the resize maths and gets a bare
9
+ // mousedown through `onDragStart`.
10
+ //
11
+ // `onSnap` is the one behaviour Gutter owns itself, because it is the same
12
+ // three lines wherever it appears: a gutter with nothing to resize yet (a
13
+ // collapsed explorer, a closed drawer) tracks its own drag and fires once on
14
+ // mouseup if the pointer travelled far enough in the opening direction —
15
+ // right for a vertical gutter, up for a horizontal one, or the reverse with
16
+ // `snapDirection: -1` (a panel that opens from the right edge). It is handed the
17
+ // distance so a caller can open to the dragged size. In that mode
18
+ // `onDragStart` is not called.
19
+ var Gutter = function Gutter(props) {
20
+ var vertical = props.orientation === 'vertical';
21
+ var onSnap = props.onSnap;
22
+
23
+ var onMouseDown = onSnap ? function (e) {
24
+ e.preventDefault();
25
+ var start = vertical ? e.clientX : e.clientY;
26
+ var threshold = props.snapThreshold == null ? 24 : props.snapThreshold;
27
+ var onMove = function (ev) { ev.preventDefault(); };
28
+ var onUp = function (ev) {
29
+ document.removeEventListener('mousemove', onMove);
30
+ document.removeEventListener('mouseup', onUp);
31
+ var dir = props.snapDirection == null ? 1 : props.snapDirection;
32
+ var moved = (vertical ? ev.clientX - start : start - ev.clientY) * dir;
33
+ if (moved >= threshold) onSnap(moved);
34
+ };
35
+ document.addEventListener('mousemove', onMove);
36
+ document.addEventListener('mouseup', onUp);
37
+ } : props.onDragStart;
38
+
39
+ return React.createElement('div', {
40
+ className: 'ide-gutter ide-gutter-' + (vertical ? 'v' : 'h') +
41
+ (props.active ? ' active' : '') +
42
+ (props.className ? ' ' + props.className : ''),
43
+ role: 'separator',
44
+ 'aria-orientation': vertical ? 'vertical' : 'horizontal',
45
+ 'aria-label': props.label,
46
+ title: props.label,
47
+ onMouseDown: onMouseDown
48
+ });
49
+ };
50
+
51
+ window.Gutter = Gutter;
@@ -88,7 +88,15 @@ var ImportDialog = function ImportDialog(_ref) {
88
88
  options.push(destOption(prefix, label, hint));
89
89
  };
90
90
 
91
- if (initialFolder) offer(initialFolder, initialFolder, 'selected folder');
91
+ // The folder this upload was started from is always first and always the
92
+ // active one. Offering it after the suggestions meant that once you had
93
+ // imported a folder anywhere, that same folder scored top of the suggestion
94
+ // list next time and sat at the head of the options — so the dialog read as
95
+ // "it remembered where I last put things". With nothing selected it was
96
+ // worse: the previous destination was the first option and the actually
97
+ // chosen one ("Workspace root") was last.
98
+ offer(initialFolder, initialFolder || 'Workspace root',
99
+ initialFolder ? 'selected folder' : 'create the folders from the root');
92
100
  suggestions.forEach(function (s) {
93
101
  var bits = [];
94
102
  if (s.files) bits.push(s.files + ' matching file' + (s.files === 1 ? '' : 's'));
@@ -48,6 +48,9 @@ function classifyLogLine(line) {
48
48
 
49
49
  var LogPanel = function LogPanel(_ref) {
50
50
  var onClose = _ref.onClose;
51
+ // Height is owned by MbeditorApp: the gutter that drags it lives between the
52
+ // editor card and this drawer, not inside it.
53
+ var height = _ref.height;
51
54
 
52
55
  var _lines = React.useState([]);
53
56
  var lines = _lines[0], setLines = _lines[1];
@@ -56,46 +59,7 @@ var LogPanel = function LogPanel(_ref) {
56
59
  var _autoScroll = React.useState(true);
57
60
  var autoScroll = _autoScroll[0], setAutoScroll = _autoScroll[1];
58
61
 
59
- var MIN_HEIGHT = 120;
60
- var _height = React.useState(function () {
61
- var saved = parseInt(window.localStorage.getItem('mbeditorLogHeight'), 10);
62
- return (saved && saved >= MIN_HEIGHT) ? saved : 240;
63
- });
64
- var height = _height[0], setHeight = _height[1];
65
-
66
62
  var bodyRef = React.useRef(null);
67
- // Mirror height into a ref so the drag end-handler can persist the final value
68
- // (setHeight is async, so reading state in onMouseUp would be stale).
69
- var heightRef = React.useRef(height);
70
- heightRef.current = height;
71
-
72
- // Drag the top edge to resize the drawer. Delta-based: track how far the
73
- // pointer has moved from where the drag started and add that to the starting
74
- // height. Dragging up grows the drawer. This is independent of where the
75
- // drawer is anchored and survives a zero/unknown viewport height.
76
- var onResizeMouseDown = function (e) {
77
- e.preventDefault();
78
- var startY = e.clientY;
79
- var startHeight = heightRef.current;
80
- var onMove = function (ev) {
81
- var vh = window.innerHeight || document.documentElement.clientHeight || 0;
82
- var maxH = vh > 0 ? Math.round(vh * 0.85) : Infinity;
83
- var next = Math.min(maxH, Math.max(MIN_HEIGHT, startHeight + (startY - ev.clientY)));
84
- heightRef.current = next;
85
- setHeight(next);
86
- };
87
- var onUp = function () {
88
- document.removeEventListener('mousemove', onMove);
89
- document.removeEventListener('mouseup', onUp);
90
- document.body.style.cursor = '';
91
- document.body.style.userSelect = '';
92
- window.localStorage.setItem('mbeditorLogHeight', String(heightRef.current));
93
- };
94
- document.body.style.cursor = 'ns-resize';
95
- document.body.style.userSelect = 'none';
96
- document.addEventListener('mousemove', onMove);
97
- document.addEventListener('mouseup', onUp);
98
- };
99
63
 
100
64
  React.useEffect(function () {
101
65
  LogService.start();
@@ -130,11 +94,6 @@ var LogPanel = function LogPanel(_ref) {
130
94
  return React.createElement(
131
95
  'div',
132
96
  { className: 'ide-log-drawer', style: { height: height + 'px' } },
133
- React.createElement('div', {
134
- className: 'ide-log-resize',
135
- title: 'Drag to resize',
136
- onMouseDown: onResizeMouseDown
137
- }),
138
97
  React.createElement(
139
98
  'div',
140
99
  { className: 'ide-log-header' },