mbeditor 0.13.1 → 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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +91 -0
  3. data/app/assets/javascripts/mbeditor/application.js +3 -0
  4. data/app/assets/javascripts/mbeditor/audit_log.js +165 -0
  5. data/app/assets/javascripts/mbeditor/collaboration_service.js +248 -26
  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 +429 -247
  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/LogPanel.js +3 -44
  17. data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +1168 -1243
  18. data/app/assets/javascripts/mbeditor/components/ModelGraph.js +94 -37
  19. data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +47 -65
  20. data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +26 -8
  21. data/app/assets/javascripts/mbeditor/components/SettingsModal.js +342 -0
  22. data/app/assets/javascripts/mbeditor/components/ShortcutHelp.js +2 -1
  23. data/app/assets/javascripts/mbeditor/components/TabBar.js +67 -98
  24. data/app/assets/javascripts/mbeditor/editor_plugins.js +687 -305
  25. data/app/assets/javascripts/mbeditor/file_import.js +13 -15
  26. data/app/assets/javascripts/mbeditor/file_service.js +38 -39
  27. data/app/assets/javascripts/mbeditor/git_service.js +15 -1
  28. data/app/assets/javascripts/mbeditor/history_service.js +5 -13
  29. data/app/assets/javascripts/mbeditor/search_service.js +9 -0
  30. data/app/assets/javascripts/mbeditor/tab_manager.js +127 -49
  31. data/app/assets/javascripts/mbeditor/websocket_service.js +13 -5
  32. data/app/assets/stylesheets/mbeditor/application.css +6 -1
  33. data/app/assets/stylesheets/mbeditor/editor.css +642 -248
  34. data/app/assets/stylesheets/mbeditor/glass.css +163 -0
  35. data/app/assets/stylesheets/mbeditor/themes.css +90 -30
  36. data/app/channels/mbeditor/collaboration_channel.rb +17 -4
  37. data/app/controllers/mbeditor/application_controller.rb +26 -3
  38. data/app/controllers/mbeditor/editors_controller.rb +117 -439
  39. data/app/services/mbeditor/archive_service.rb +137 -0
  40. data/app/services/mbeditor/collaboration_doc_store.rb +143 -14
  41. data/app/services/mbeditor/editor_state_service.rb +14 -51
  42. data/app/services/mbeditor/file_history_service.rb +222 -0
  43. data/app/services/mbeditor/git_info_service.rb +6 -0
  44. data/app/services/mbeditor/js_syntax_check_service.rb +42 -16
  45. data/app/services/mbeditor/lint_service.rb +137 -0
  46. data/app/services/mbeditor/locked_json_file.rb +67 -0
  47. data/app/services/mbeditor/process_runner.rb +32 -0
  48. data/app/services/mbeditor/ruby_lsp_result_translator.rb +226 -0
  49. data/app/services/mbeditor/search_replace_service.rb +8 -0
  50. data/app/views/layouts/mbeditor/application.html.erb +1 -1
  51. data/lib/mbeditor/audit_log.rb +203 -0
  52. data/lib/mbeditor/configuration.rb +6 -1
  53. data/lib/mbeditor/rack/pending_migration_bypass.rb +15 -9
  54. data/lib/mbeditor/route_map.rb +4 -0
  55. data/lib/mbeditor/ruby_lsp_client.rb +82 -14
  56. data/lib/mbeditor/version.rb +1 -1
  57. data/lib/mbeditor.rb +1 -0
  58. metadata +12 -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 = el.getBoundingClientRect();
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
- var sel = '[data-from="' + name + '"],[data-to="' + name + '"]';
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
- setFocused(name);
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: function (ev) {
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
- className: 'mg-node' + (focused === m.name ? ' mg-focused' : ''),
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, focused]);
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: pointer.x + (svgRef.current && pointer.x > svgRef.current.clientWidth - 260 ? -240 : 14) + 'px',
1066
- top: (pointer.y + 14) + 'px'
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' },
@@ -44,6 +44,32 @@ var ProblemsPanel = (function () {
44
44
  return text.length > CODE_PREVIEW_LIMIT ? text.slice(0, CODE_PREVIEW_LIMIT) + '…' : text;
45
45
  }
46
46
 
47
+ // VS Code-style file group header: chevron, file-type icon, basename, then
48
+ // the containing directory in a dimmer colour. `path` is workspace-relative.
49
+ function fileGroupName(path) {
50
+ var slash = path.lastIndexOf('/');
51
+ return {
52
+ base: slash === -1 ? path : path.slice(slash + 1),
53
+ dir: slash === -1 ? '' : path.slice(0, slash)
54
+ };
55
+ }
56
+
57
+ function renderFileGroupHeader(path, count) {
58
+ var parts = fileGroupName(path);
59
+ return React.createElement(
60
+ 'div',
61
+ { className: 'ide-problems-file-name' },
62
+ React.createElement('i', { className: 'fas fa-chevron-down', 'aria-hidden': 'true' }),
63
+ React.createElement('i', {
64
+ className: (window.getFileIcon ? window.getFileIcon(parts.base) : 'far fa-file-code'),
65
+ 'aria-hidden': 'true'
66
+ }),
67
+ React.createElement('span', null, parts.base),
68
+ parts.dir && React.createElement('span', { className: 'ide-problems-dir' }, parts.dir),
69
+ React.createElement('span', { className: 'ide-problems-file-count' }, count)
70
+ );
71
+ }
72
+
47
73
  // Reading markers means walking every model, so callers that only want the
48
74
  // counts share this one pass. Exposed on the component for the status bar.
49
75
  function collect() {
@@ -111,6 +137,9 @@ var ProblemsPanel = (function () {
111
137
 
112
138
  var Panel = function ProblemsPanelComponent(_ref) {
113
139
  var onClose = _ref.onClose;
140
+ // Height is owned by MbeditorApp: the gutter that drags it lives between
141
+ // the editor card and this drawer, not inside it.
142
+ var height = _ref.height;
114
143
  var onOpenFile = _ref.onOpenFile;
115
144
  // Called after `-a` has rewritten files on disk, so the app can re-read
116
145
  // every open tab. Nothing else would: the write came from a subprocess,
@@ -226,34 +255,6 @@ var ProblemsPanel = (function () {
226
255
  });
227
256
  };
228
257
 
229
- var MIN_HEIGHT = 120;
230
- var _height = React.useState(function () {
231
- var saved = parseInt(window.localStorage.getItem('mbeditorProblemsHeight'), 10);
232
- return (saved && saved >= MIN_HEIGHT) ? saved : 240;
233
- });
234
- var height = _height[0], setHeight = _height[1];
235
- var heightRef = React.useRef(height);
236
- heightRef.current = height;
237
-
238
- // Same delta-based resize as the log drawer.
239
- var onResizeMouseDown = function (e) {
240
- e.preventDefault();
241
- var startY = e.clientY;
242
- var startHeight = heightRef.current;
243
- var onMove = function (ev) {
244
- var vh = window.innerHeight || document.documentElement.clientHeight || 0;
245
- var maxH = vh > 0 ? Math.round(vh * 0.85) : Infinity;
246
- setHeight(Math.min(maxH, Math.max(MIN_HEIGHT, startHeight + (startY - ev.clientY))));
247
- };
248
- var onUp = function () {
249
- document.removeEventListener('mousemove', onMove);
250
- document.removeEventListener('mouseup', onUp);
251
- window.localStorage.setItem('mbeditorProblemsHeight', String(heightRef.current));
252
- };
253
- document.addEventListener('mousemove', onMove);
254
- document.addEventListener('mouseup', onUp);
255
- };
256
-
257
258
  React.useEffect(function () {
258
259
  if (!window.monaco || !window.monaco.editor) return;
259
260
  // Debounced for the same reason the status-bar tally is: markers change
@@ -314,16 +315,11 @@ var ProblemsPanel = (function () {
314
315
  return React.createElement(
315
316
  'div',
316
317
  { className: 'ide-problems-drawer', style: { height: height + 'px' } },
317
- React.createElement('div', {
318
- className: 'ide-problems-resize',
319
- title: 'Drag to resize',
320
- onMouseDown: onResizeMouseDown
321
- }),
322
318
  React.createElement(
323
319
  'div',
324
320
  { className: 'ide-problems-header' },
325
- React.createElement('i', { className: 'fas fa-bug' }),
326
321
  React.createElement('span', { className: 'ide-problems-title' }, 'Problems'),
322
+ React.createElement('span', { className: 'ide-problems-total-pill' }, total + wsCounts.error + wsCounts.warning + wsCounts.info),
327
323
  // No prose summary: the severity chips below carry the same three
328
324
  // counts, and restating them was the longest thing in the header.
329
325
  React.createElement(
@@ -340,7 +336,7 @@ var ProblemsPanel = (function () {
340
336
  {
341
337
  key: s.kind,
342
338
  type: 'button',
343
- className: 'ide-problems-sev-chip ide-problems-sev-' + s.kind + (on ? ' is-on' : ''),
339
+ className: 'ide-problems-sev-chip' + (on ? ' is-on' : ''),
344
340
  title: (on ? 'Hide' : 'Show') + ' ' + SEVERITY_LABEL[s.kind].toLowerCase() + 's',
345
341
  'aria-pressed': on ? 'true' : 'false',
346
342
  onClick: function () { toggleSeverity(s.kind); }
@@ -350,19 +346,25 @@ var ProblemsPanel = (function () {
350
346
  );
351
347
  })
352
348
  ),
349
+ React.createElement('input', {
350
+ className: 'ide-problems-filter',
351
+ type: 'text',
352
+ placeholder: 'Filter…',
353
+ value: filter,
354
+ onChange: function (e) { setFilter(e.target.value); }
355
+ }),
353
356
  React.createElement(
354
357
  'div',
355
358
  { className: 'ide-problems-actions' },
356
359
  React.createElement('button', {
357
- type: 'button', className: 'ide-problems-btn',
360
+ type: 'button', className: 'ide-icon-btn',
358
361
  disabled: !!running,
359
362
  title: 'Run rubocop over the whole workspace',
360
363
  onClick: function () { runRubocop('check'); }
361
364
  },
362
- React.createElement('i', { className: running === 'check' ? 'fas fa-spinner fa-spin' : 'fas fa-play' }),
363
- React.createElement('span', null, ' RuboCop')),
365
+ React.createElement('i', { className: running === 'check' ? 'fas fa-spinner fa-spin' : 'fas fa-play' })),
364
366
  React.createElement('button', {
365
- type: 'button', className: 'ide-problems-btn',
367
+ type: 'button', className: 'ide-icon-btn',
366
368
  // Nothing to correct until a run says so, and rerunning `-a` on a
367
369
  // clean tree is a slow no-op.
368
370
  disabled: !!running || !workspace || !workspace.correctable,
@@ -373,28 +375,19 @@ var ProblemsPanel = (function () {
373
375
  : 'Run RuboCop first',
374
376
  onClick: function () { runRubocop('autocorrect'); }
375
377
  },
376
- React.createElement('i', { className: running === 'autocorrect' ? 'fas fa-spinner fa-spin' : 'fas fa-magic' }),
377
- React.createElement('span', null, ' Fix')),
378
+ React.createElement('i', { className: running === 'autocorrect' ? 'fas fa-spinner fa-spin' : 'fas fa-magic' })),
378
379
  React.createElement('button', {
379
- type: 'button', className: 'ide-problems-btn',
380
+ type: 'button', className: 'ide-icon-btn',
380
381
  disabled: !workspace || !(workspace.files || []).length,
381
382
  title: workspace && (workspace.files || []).length
382
383
  ? 'Open all ' + workspace.files.length + ' file(s) with offenses'
383
384
  : 'Run RuboCop first',
384
385
  onClick: openAllOffending
385
386
  },
386
- React.createElement('i', { className: 'fas fa-folder-open' }),
387
- React.createElement('span', null, ' Open all'))
387
+ React.createElement('i', { className: 'fas fa-folder-open' }))
388
388
  ),
389
- React.createElement('input', {
390
- className: 'ide-problems-filter',
391
- type: 'text',
392
- placeholder: 'Filter…',
393
- value: filter,
394
- onChange: function (e) { setFilter(e.target.value); }
395
- }),
396
389
  React.createElement('button', {
397
- type: 'button', className: 'ide-problems-btn',
390
+ type: 'button', className: 'ide-icon-btn',
398
391
  title: 'Close', onClick: onClose
399
392
  }, React.createElement('i', { className: 'fas fa-times' }))
400
393
  ),
@@ -412,13 +405,7 @@ var ProblemsPanel = (function () {
412
405
  return React.createElement(
413
406
  'div',
414
407
  { className: 'ide-problems-file', key: 'ws:' + file.path },
415
- React.createElement(
416
- 'div',
417
- { className: 'ide-problems-file-name' },
418
- React.createElement('i', { className: 'fas fa-gavel', 'aria-hidden': 'true' }),
419
- ' ' + file.path,
420
- React.createElement('span', { className: 'ide-problems-file-count' }, file.offenses.length)
421
- ),
408
+ renderFileGroupHeader(file.path, file.offenses.length),
422
409
  file.offenses.map(function (o, i) {
423
410
  var kind = COP_SEVERITY_KIND[o.severity] || 'info';
424
411
  return React.createElement(
@@ -502,12 +489,7 @@ var ProblemsPanel = (function () {
502
489
  return React.createElement(
503
490
  'div',
504
491
  { className: 'ide-problems-file', key: entry.path },
505
- React.createElement(
506
- 'div',
507
- { className: 'ide-problems-file-name' },
508
- entry.path,
509
- React.createElement('span', { className: 'ide-problems-file-count' }, entry.markers.length)
510
- ),
492
+ renderFileGroupHeader(entry.path, entry.markers.length),
511
493
  entry.markers.map(function (item, index) {
512
494
  var marker = item.marker;
513
495
  var kind = SEVERITY_KIND[marker.severity] || 'info';
@@ -65,10 +65,19 @@ function recentRanks() {
65
65
  // in — a typo'd or transposed query hits nothing literally, so an exact match
66
66
  // can never be demoted by loosening the index search.
67
67
  var FUZZY_ONLY = 4;
68
+
69
+ // CamelCase -> snake_case, so a query like "ThemeController" ranks against
70
+ // "theme_controller.rb" the same way "theme_controller" would. A no-op
71
+ // (beyond lowercasing) for queries with no lower-to-upper transition, so
72
+ // non-CamelCase ranking is unchanged.
73
+ function toSnakeCase(q) {
74
+ return (q || '').replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
75
+ }
76
+
68
77
  function getMatchRelevance(result, q) {
69
78
  if (!q) return FUZZY_ONLY;
70
79
  var name = (result.name || (result.path || '').split('/').pop() || '').toLowerCase();
71
- var lq = q.toLowerCase();
80
+ var lq = toSnakeCase(q);
72
81
  if (name === lq) return 0;
73
82
  if (name.slice(0, lq.length) === lq) return 1;
74
83
  if (name.indexOf(lq) >= 0) return 2;
@@ -194,7 +203,17 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
194
203
  // path in the workspace ran on every keystroke, between the keypress and
195
204
  // the character appearing.
196
205
  var timer = setTimeout(function () {
197
- var filtered = rankResults(SearchService.searchFiles(query), query, showFolders);
206
+ var hits = SearchService.searchFiles(query);
207
+ // CamelCase query, e.g. "ThemeController": also search its snake_case
208
+ // form so it finds theme_controller.rb, whose index terms are snake_case.
209
+ var snake = toSnakeCase(query);
210
+ if (snake !== query.toLowerCase()) {
211
+ var seen = new Set(hits.map(function (r) { return r.path; }));
212
+ SearchService.searchFiles(snake).forEach(function (r) {
213
+ if (!seen.has(r.path)) { seen.add(r.path); hits.push(r); }
214
+ });
215
+ }
216
+ var filtered = rankResults(hits, query, showFolders);
198
217
  setResults(filtered.slice(0, 200));
199
218
  setSelectedIndex(0);
200
219
  }, 120);
@@ -291,7 +310,7 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
291
310
  return React.createElement(
292
311
  'div',
293
312
  { className: 'quick-open-section' },
294
- React.createElement('div', { className: 'quick-open-section-header' },
313
+ React.createElement('div', { className: 'quick-open-section-header quick-open-section-header-muted' },
295
314
  React.createElement('i', { className: 'fas fa-history', style: { marginRight: '6px', fontSize: '10px' } }),
296
315
  'Recent Searches'
297
316
  ),
@@ -303,6 +322,8 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
303
322
  var recentFiles = (typeof TabManager !== 'undefined' && TabManager.getRecentFiles)
304
323
  ? TabManager.getRecentFiles() : [];
305
324
  if (recentFiles.length === 0) return null;
325
+ // No section header here (unlike Favourites/Recent Searches) — VS Code
326
+ // marks each row inline with a trailing "recently opened" label instead.
306
327
  var rows = recentFiles.map(function (entry) {
307
328
  var path = entry.path;
308
329
  var name = entry.name || (path.split('/').pop());
@@ -320,16 +341,13 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
320
341
  React.createElement('div', { className: 'quick-open-result-name' }, name),
321
342
  React.createElement('div', { className: 'quick-open-result-path' }, path)
322
343
  ),
344
+ React.createElement('span', { className: 'quick-open-recent-label' }, 'recently opened'),
323
345
  renderStarBtn(path)
324
346
  );
325
347
  });
326
348
  return React.createElement(
327
349
  'div',
328
350
  { className: 'quick-open-section' },
329
- React.createElement('div', { className: 'quick-open-section-header' },
330
- React.createElement('i', { className: 'fas fa-clock', style: { marginRight: '6px', fontSize: '10px' } }),
331
- 'Recently Opened'
332
- ),
333
351
  rows
334
352
  );
335
353
  }
@@ -349,7 +367,7 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
349
367
  ref: inputRef,
350
368
  type: 'text',
351
369
  className: 'quick-open-input',
352
- placeholder: 'Search files by name (Ctrl+P)…',
370
+ placeholder: 'Search files by name',
353
371
  value: query,
354
372
  onChange: function (e) { setQuery(e.target.value); },
355
373
  onKeyDown: handleKeyDown