mbeditor 0.3.8 → 0.3.9

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.
@@ -24,6 +24,8 @@ var DEFAULT_EDITOR_PREFS = {
24
24
  theme: 'vs-dark',
25
25
  fontSize: 13,
26
26
  fontFamily: "'JetBrains Mono', 'Fira Code', Consolas, 'Courier New', monospace",
27
+ lineHeight: 0,
28
+ letterSpacing: 0,
27
29
  tabSize: 4,
28
30
  insertSpaces: false,
29
31
  wordWrap: 'off',
@@ -32,6 +34,20 @@ var DEFAULT_EDITOR_PREFS = {
32
34
  scrollBeyondLastLine: false,
33
35
  minimap: false,
34
36
  bracketPairColorization: true,
37
+ renderLineHighlight: 'none',
38
+ cursorStyle: 'line',
39
+ cursorBlinking: 'blink',
40
+ folding: true,
41
+ smoothScrolling: false,
42
+ mouseWheelZoom: false,
43
+ autoClosingBrackets: 'always',
44
+ autoClosingQuotes: 'always',
45
+ autoIndent: 'full',
46
+ formatOnPaste: true,
47
+ formatOnType: true,
48
+ quickSuggestions: true,
49
+ wordBasedSuggestions: 'matchingDocuments',
50
+ acceptSuggestionOnEnter: 'on',
35
51
  autoRevealInExplorer: true,
36
52
  toolbarIconOnly: false,
37
53
  rubocopLintEnabled: true,
@@ -170,20 +186,17 @@ var MbeditorApp = function MbeditorApp() {
170
186
  var searchHasMore = _useState33h2[0];
171
187
  var setSearchHasMore = _useState33h2[1];
172
188
 
173
- var _useState33i = useState(false);
174
- var _useState33i2 = _slicedToArray(_useState33i, 2);
175
- var searchLoadingMore = _useState33i2[0];
176
- var setSearchLoadingMore = _useState33i2[1];
189
+ var _useState33tc = useState(0);
190
+ var _useState33tc2 = _slicedToArray(_useState33tc, 2);
191
+ var searchTotalCount = _useState33tc2[0];
192
+ var setSearchTotalCount = _useState33tc2[1];
177
193
 
178
- var searchOffsetRef = useRef(0); // tracks next offset to load
179
- var searchQueryRef = useRef(''); // tracks query that produced current results
180
- var searchResultsContainerRef = useRef(null);
181
- var searchVirtStartRef = useRef(0); // first visible item index (for virtual list)
194
+ var searchHasMoreRef = useRef(false);
195
+ var searchOffsetRef = useRef(0);
196
+ var searchLoadingMoreRef = useRef(false);
182
197
 
183
- var _useState33j = useState(0);
184
- var _useState33j2 = _slicedToArray(_useState33j, 2);
185
- var searchVirtStart = _useState33j2[0];
186
- var setSearchVirtStart = _useState33j2[1];
198
+ var searchQueryRef = useRef('');
199
+ var searchResultsContainerRef = useRef(null);
187
200
 
188
201
  var _useState8 = useState("explorer");
189
202
 
@@ -1481,40 +1494,61 @@ var MbeditorApp = function MbeditorApp() {
1481
1494
  var onFormatRef = useRef(handleFormat);
1482
1495
  onFormatRef.current = handleFormat;
1483
1496
 
1497
+ // Eagerly load all remaining pages sequentially in the background.
1498
+ // Self-chains via .then() so only one request is in-flight at a time.
1499
+ // Uses only refs so it's safe to call from async callbacks without
1500
+ // worrying about stale closure state.
1484
1501
  var _debouncedSearch = useRef(window._.debounce(function (q) {
1485
1502
  if (!q.trim()) {
1486
1503
  searchRequestIdRef.current += 1;
1487
1504
  setSearchLoading(false);
1488
- setSearchHasMore(false);
1505
+ setSearchHasMore(false); searchHasMoreRef.current = false;
1506
+ setSearchTotalCount(0);
1489
1507
  searchOffsetRef.current = 0;
1508
+ searchLoadingMoreRef.current = false;
1490
1509
  searchQueryRef.current = '';
1491
1510
  EditorStore.setState({ searchResults: [], searchHasMore: false });
1492
1511
  return;
1493
1512
  }
1494
1513
  var requestId = ++searchRequestIdRef.current;
1495
1514
  setSearchLoading(true);
1496
- setSearchHasMore(false);
1515
+ setSearchHasMore(false); searchHasMoreRef.current = false;
1516
+ setSearchTotalCount(0);
1497
1517
  searchOffsetRef.current = 0;
1518
+ searchLoadingMoreRef.current = false;
1498
1519
  searchQueryRef.current = q;
1499
- setSearchVirtStart(0);
1500
- searchVirtStartRef.current = 0;
1501
1520
  EditorStore.setState({ searchResults: [], searchHasMore: false });
1502
1521
  EditorStore.setStatus("Searching project...", "info");
1503
1522
  SearchService.projectSearch(q, 0, SearchService.PAGE_SIZE).then(function (res) {
1504
- if (searchRequestIdRef.current === requestId) {
1505
- var hasMore = !!(res && res.hasMore);
1506
- setSearchHasMore(hasMore);
1507
- searchOffsetRef.current = SearchService.PAGE_SIZE;
1508
- var count = res && res.results ? res.results.length : 0;
1509
- EditorStore.setStatus("Found " + count + (hasMore ? '+' : '') + " result" + (count !== 1 ? "s" : ""), "success");
1510
- }
1523
+ if (searchRequestIdRef.current !== requestId) return;
1524
+ var hasMore = !!(res && res.hasMore);
1525
+ setSearchHasMore(hasMore); searchHasMoreRef.current = hasMore;
1526
+ searchOffsetRef.current = SearchService.PAGE_SIZE;
1527
+ if (res && res.totalCount != null) setSearchTotalCount(res.totalCount);
1528
+ var total = (res && res.totalCount != null) ? res.totalCount : (res && res.results ? res.results.length : 0);
1529
+ EditorStore.setStatus("Found " + total + (hasMore ? '+' : '') + " result" + (total !== 1 ? "s" : ""), "success");
1511
1530
  }).finally(function () {
1512
- if (searchRequestIdRef.current === requestId) {
1513
- setSearchLoading(false);
1514
- }
1531
+ if (searchRequestIdRef.current === requestId) setSearchLoading(false);
1515
1532
  });
1516
1533
  }, 400)).current;
1517
1534
 
1535
+ var loadMoreSearchResults = function loadMoreSearchResults() {
1536
+ var q = searchQueryRef.current;
1537
+ if (!q || searchLoadingMoreRef.current || !searchHasMoreRef.current) return;
1538
+ searchLoadingMoreRef.current = true;
1539
+ var offset = searchOffsetRef.current;
1540
+ SearchService.projectSearch(q, offset, SearchService.PAGE_SIZE).then(function(res) {
1541
+ if (searchQueryRef.current !== q) { searchLoadingMoreRef.current = false; return; }
1542
+ var hasMore = !!(res && res.hasMore);
1543
+ searchHasMoreRef.current = hasMore;
1544
+ setSearchHasMore(hasMore);
1545
+ searchOffsetRef.current = offset + SearchService.PAGE_SIZE;
1546
+ searchLoadingMoreRef.current = false;
1547
+ }).catch(function() {
1548
+ searchLoadingMoreRef.current = false;
1549
+ });
1550
+ };
1551
+
1518
1552
  var handleSearchChange = function handleSearchChange(e) {
1519
1553
  var val = e.target.value;
1520
1554
  if (!val) { clearSearch(); return; }
@@ -1527,12 +1561,11 @@ var MbeditorApp = function MbeditorApp() {
1527
1561
  if (_debouncedSearch.cancel) _debouncedSearch.cancel();
1528
1562
  setSearchQuery("");
1529
1563
  setSearchLoading(false);
1530
- setSearchHasMore(false);
1531
- setSearchLoadingMore(false);
1564
+ setSearchHasMore(false); searchHasMoreRef.current = false;
1565
+ setSearchTotalCount(0);
1532
1566
  searchOffsetRef.current = 0;
1567
+ searchLoadingMoreRef.current = false;
1533
1568
  searchQueryRef.current = '';
1534
- setSearchVirtStart(0);
1535
- searchVirtStartRef.current = 0;
1536
1569
  EditorStore.setState({ searchResults: [], searchHasMore: false });
1537
1570
  };
1538
1571
 
@@ -1541,35 +1574,11 @@ var MbeditorApp = function MbeditorApp() {
1541
1574
  _debouncedSearch(searchQuery);
1542
1575
  };
1543
1576
 
1544
- var loadMoreSearchResults = function loadMoreSearchResults() {
1545
- var q = searchQueryRef.current;
1546
- if (!q || searchLoadingMore || !searchHasMore) return;
1547
- var offset = searchOffsetRef.current;
1548
- setSearchLoadingMore(true);
1549
- SearchService.projectSearch(q, offset, SearchService.PAGE_SIZE).then(function (res) {
1550
- var hasMore = !!(res && res.hasMore);
1551
- setSearchHasMore(hasMore);
1552
- searchOffsetRef.current = offset + SearchService.PAGE_SIZE;
1553
- }).finally(function () {
1554
- setSearchLoadingMore(false);
1555
- });
1556
- };
1557
-
1558
- // Scroll handler for the virtualized search results list.
1559
- var SEARCH_ITEM_H = 40;
1560
- var SEARCH_OVERSCAN = 8;
1577
+ // Load more results when the user scrolls near the bottom of the list.
1561
1578
  var handleSearchResultsScroll = function handleSearchResultsScroll(e) {
1562
1579
  var el = e.currentTarget;
1563
- var newStart = Math.floor(el.scrollTop / SEARCH_ITEM_H);
1564
- if (Math.abs(newStart - searchVirtStartRef.current) >= 4) {
1565
- searchVirtStartRef.current = newStart;
1566
- setSearchVirtStart(newStart);
1567
- }
1568
- // Trigger load-more when within 200px of the bottom
1569
- if (!searchLoadingMore && searchHasMore) {
1570
- if (el.scrollHeight - el.scrollTop - el.clientHeight < 200) {
1571
- loadMoreSearchResults();
1572
- }
1580
+ if (el.scrollHeight - el.scrollTop - el.clientHeight < 200) {
1581
+ loadMoreSearchResults();
1573
1582
  }
1574
1583
  };
1575
1584
 
@@ -2523,88 +2532,58 @@ var MbeditorApp = function MbeditorApp() {
2523
2532
  React.createElement("i", { className: searchLoading ? "fas fa-spinner fa-spin" : "fas fa-search" })
2524
2533
  )
2525
2534
  ),
2526
- React.createElement(
2527
- "div",
2528
- {
2529
- className: "search-results",
2530
- ref: searchResultsContainerRef,
2531
- onScroll: handleSearchResultsScroll
2532
- },
2533
- (function() {
2534
- var allResults = state.searchResults || [];
2535
- var totalCount = allResults.length;
2536
-
2537
- if (searchQuery && totalCount === 0 && !searchLoading) {
2538
- return React.createElement(
2539
- "div", { className: "search-results-empty" }, "No results"
2540
- );
2541
- }
2542
-
2543
- if (totalCount === 0) return null;
2535
+ (function() {
2536
+ var allResults = state.searchResults || [];
2537
+ var loadedCount = allResults.length;
2538
+ var total = searchTotalCount > 0 ? searchTotalCount : loadedCount;
2539
+ var hasAny = loadedCount > 0;
2544
2540
 
2545
- // Compute virtual window
2546
- var containerEl = searchResultsContainerRef.current;
2547
- var containerH = containerEl ? containerEl.clientHeight : 400;
2548
- var visibleCount = Math.ceil(containerH / SEARCH_ITEM_H) + SEARCH_OVERSCAN * 2;
2549
- var virtStart = Math.max(0, searchVirtStart - SEARCH_OVERSCAN);
2550
- var virtEnd = Math.min(totalCount, virtStart + visibleCount);
2551
- var paddingTop = virtStart * SEARCH_ITEM_H;
2552
- var paddingBottom = Math.max(0, (totalCount - virtEnd) * SEARCH_ITEM_H);
2553
-
2554
- var visibleItems = allResults.slice(virtStart, virtEnd).map(function(res, idx) {
2555
- var absoluteIdx = virtStart + idx;
2556
- var fileName = res.file.split('/').pop();
2557
- return React.createElement(
2558
- "div",
2559
- {
2560
- key: absoluteIdx,
2561
- className: "search-result-item",
2562
- onClick: function() { handleSelectFile(res.file, res.file.split('/').pop(), res.line); }
2563
- },
2564
- React.createElement("i", { className: (window.getFileIcon ? window.getFileIcon(fileName) : 'far fa-file-code') + " search-result-icon" }),
2565
- React.createElement(
2541
+ return React.createElement(
2542
+ React.Fragment,
2543
+ null,
2544
+ searchQuery && !searchLoading && React.createElement(
2545
+ "div",
2546
+ { className: "search-results-header" },
2547
+ hasAny
2548
+ ? (total + (searchHasMore ? '+' : '') + " result" + (total !== 1 ? "s" : ""))
2549
+ : "No results"
2550
+ ),
2551
+ hasAny && React.createElement(
2552
+ "div",
2553
+ {
2554
+ className: "search-results",
2555
+ ref: searchResultsContainerRef,
2556
+ onScroll: handleSearchResultsScroll
2557
+ },
2558
+ allResults.map(function(res, i) {
2559
+ var fileName = res.file.split('/').pop();
2560
+ return React.createElement(
2566
2561
  "div",
2567
- { className: "search-result-body" },
2562
+ {
2563
+ key: i,
2564
+ className: "search-result-item",
2565
+ onClick: (function(r) { return function() { handleSelectFile(r.file, r.file.split('/').pop(), r.line); }; })(res)
2566
+ },
2567
+ React.createElement("i", { className: (window.getFileIcon ? window.getFileIcon(fileName) : 'far fa-file-code') + " search-result-icon" }),
2568
2568
  React.createElement(
2569
- "div",
2570
- { className: "search-result-file" },
2571
- fileName,
2569
+ "div", { className: "search-result-body" },
2572
2570
  React.createElement(
2573
- "span",
2574
- { className: "search-result-line-num" },
2575
- " ", res.file, ":", res.line
2576
- )
2577
- ),
2578
- React.createElement(
2579
- "div",
2580
- { className: "search-result-text" },
2581
- res.text
2571
+ "div", { className: "search-result-file" },
2572
+ fileName,
2573
+ React.createElement("span", { className: "search-result-line-num" }, " ", res.file, ":", res.line)
2574
+ ),
2575
+ React.createElement("div", { className: "search-result-text" }, res.text)
2582
2576
  )
2583
- )
2584
- );
2585
- });
2586
-
2587
- return React.createElement(
2588
- React.Fragment,
2589
- null,
2590
- React.createElement(
2591
- "div",
2592
- { className: "search-results-meta" },
2593
- totalCount,
2594
- " result" + (totalCount !== 1 ? "s" : ""),
2595
- searchHasMore && React.createElement("span", { className: "search-results-capped" }, " — more available, scroll for next page")
2596
- ),
2597
- React.createElement("div", { style: { height: paddingTop + 'px', flexShrink: 0 } }),
2598
- visibleItems,
2599
- React.createElement("div", { style: { height: paddingBottom + 'px', flexShrink: 0 } }),
2600
- searchLoadingMore && React.createElement(
2601
- "div",
2602
- { className: "search-results-loading-more", 'aria-busy': 'true' },
2603
- 'Loading more…'
2577
+ );
2578
+ }),
2579
+ searchHasMore && React.createElement(
2580
+ "div", { className: "search-loading-more" },
2581
+ React.createElement("i", { className: "fas fa-spinner fa-spin" }),
2582
+ " Loading more\u2026"
2604
2583
  )
2605
- );
2606
- })()
2607
- )
2584
+ )
2585
+ );
2586
+ })()
2608
2587
  )
2609
2588
  )
2610
2589
  ),
@@ -2735,6 +2714,32 @@ var MbeditorApp = function MbeditorApp() {
2735
2714
  onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { fontFamily: e.target.value }); }); }
2736
2715
  })
2737
2716
  ),
2717
+ React.createElement(
2718
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Row height in pixels. 0 = auto (roughly font size × 1.5)' },
2719
+ React.createElement('span', { className: 'ide-settings-label' }, 'Line height (0=auto)'),
2720
+ React.createElement('input', {
2721
+ type: 'number', min: '0', max: '100', step: '1',
2722
+ className: 'ide-settings-input',
2723
+ value: editorPrefs.lineHeight != null ? editorPrefs.lineHeight : 0,
2724
+ onChange: function(e) {
2725
+ var v = parseInt(e.target.value, 10);
2726
+ if (!isNaN(v) && v >= 0 && v <= 100) setEditorPrefs(function(p) { return Object.assign({}, p, { lineHeight: v }); });
2727
+ }
2728
+ })
2729
+ ),
2730
+ React.createElement(
2731
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Extra space between characters in pixels. 0 = default' },
2732
+ React.createElement('span', { className: 'ide-settings-label' }, 'Letter spacing (px)'),
2733
+ React.createElement('input', {
2734
+ type: 'number', min: '-5', max: '20', step: '0.5',
2735
+ className: 'ide-settings-input',
2736
+ value: editorPrefs.letterSpacing != null ? editorPrefs.letterSpacing : 0,
2737
+ onChange: function(e) {
2738
+ var v = parseFloat(e.target.value);
2739
+ if (!isNaN(v) && v >= -5 && v <= 20) setEditorPrefs(function(p) { return Object.assign({}, p, { letterSpacing: v }); });
2740
+ }
2741
+ })
2742
+ ),
2738
2743
 
2739
2744
  /* ── Indentation (unified editor + Prettier) ── */
2740
2745
  React.createElement('div', { className: 'ide-settings-section-header' }, 'Indentation'),
@@ -2844,6 +2849,184 @@ var MbeditorApp = function MbeditorApp() {
2844
2849
  onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { vimMode: v }); }); }
2845
2850
  })
2846
2851
  ),
2852
+ React.createElement(
2853
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'When to insert a matching closing bracket automatically' },
2854
+ React.createElement('span', { className: 'ide-settings-label' }, 'Auto-close brackets'),
2855
+ React.createElement(
2856
+ 'select', {
2857
+ value: editorPrefs.autoClosingBrackets || 'always',
2858
+ onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { autoClosingBrackets: e.target.value }); }); }
2859
+ },
2860
+ React.createElement('option', { value: 'always' }, 'Always'),
2861
+ React.createElement('option', { value: 'languageDefined' }, 'Per language rules'),
2862
+ React.createElement('option', { value: 'beforeWhitespace' }, 'Only before whitespace'),
2863
+ React.createElement('option', { value: 'never' }, 'Never')
2864
+ )
2865
+ ),
2866
+ React.createElement(
2867
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'When to insert a matching closing quote automatically' },
2868
+ React.createElement('span', { className: 'ide-settings-label' }, 'Auto-close quotes'),
2869
+ React.createElement(
2870
+ 'select', {
2871
+ value: editorPrefs.autoClosingQuotes || 'always',
2872
+ onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { autoClosingQuotes: e.target.value }); }); }
2873
+ },
2874
+ React.createElement('option', { value: 'always' }, 'Always'),
2875
+ React.createElement('option', { value: 'languageDefined' }, 'Per language rules'),
2876
+ React.createElement('option', { value: 'beforeWhitespace' }, 'Only before whitespace'),
2877
+ React.createElement('option', { value: 'never' }, 'Never')
2878
+ )
2879
+ ),
2880
+ React.createElement(
2881
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'What to highlight on the current editor line' },
2882
+ React.createElement('span', { className: 'ide-settings-label' }, 'Line highlight'),
2883
+ React.createElement(
2884
+ 'select', {
2885
+ value: editorPrefs.renderLineHighlight || 'none',
2886
+ onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { renderLineHighlight: e.target.value }); }); }
2887
+ },
2888
+ React.createElement('option', { value: 'none' }, 'None'),
2889
+ React.createElement('option', { value: 'gutter' }, 'Line number only'),
2890
+ React.createElement('option', { value: 'line' }, 'Current line background'),
2891
+ React.createElement('option', { value: 'all' }, 'Line number + background')
2892
+ )
2893
+ ),
2894
+ React.createElement(
2895
+ 'label', { className: 'ide-settings-row ide-settings-row-half' },
2896
+ React.createElement('span', { className: 'ide-settings-label' }, 'Cursor style'),
2897
+ React.createElement(
2898
+ 'select', {
2899
+ value: editorPrefs.cursorStyle || 'line',
2900
+ onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { cursorStyle: e.target.value }); }); }
2901
+ },
2902
+ React.createElement('option', { value: 'line' }, 'Line (|)'),
2903
+ React.createElement('option', { value: 'block' }, 'Block (filled)'),
2904
+ React.createElement('option', { value: 'underline' }, 'Underline (_)'),
2905
+ React.createElement('option', { value: 'line-thin' }, 'Line thin'),
2906
+ React.createElement('option', { value: 'block-outline' }, 'Block outline'),
2907
+ React.createElement('option', { value: 'underline-thin' }, 'Underline thin')
2908
+ )
2909
+ ),
2910
+ React.createElement(
2911
+ 'label', { className: 'ide-settings-row ide-settings-row-half' },
2912
+ React.createElement('span', { className: 'ide-settings-label' }, 'Cursor blinking'),
2913
+ React.createElement(
2914
+ 'select', {
2915
+ value: editorPrefs.cursorBlinking || 'blink',
2916
+ onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { cursorBlinking: e.target.value }); }); }
2917
+ },
2918
+ React.createElement('option', { value: 'blink' }, 'Blink (on/off)'),
2919
+ React.createElement('option', { value: 'smooth' }, 'Smooth (fade)'),
2920
+ React.createElement('option', { value: 'phase' }, 'Phase (offset fade)'),
2921
+ React.createElement('option', { value: 'expand' }, 'Expand (grow/shrink)'),
2922
+ React.createElement('option', { value: 'solid' }, 'Solid (no blink)')
2923
+ )
2924
+ ),
2925
+ React.createElement(
2926
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Show collapse arrows next to foldable regions (functions, classes, blocks)' },
2927
+ React.createElement('span', { className: 'ide-settings-label' }, 'Code folding'),
2928
+ React.createElement('input', {
2929
+ type: 'checkbox',
2930
+ className: 'ide-settings-checkbox',
2931
+ checked: editorPrefs.folding !== false,
2932
+ onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { folding: v }); }); }
2933
+ })
2934
+ ),
2935
+ React.createElement(
2936
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Animate scrolling instead of jumping instantly' },
2937
+ React.createElement('span', { className: 'ide-settings-label' }, 'Smooth scrolling'),
2938
+ React.createElement('input', {
2939
+ type: 'checkbox',
2940
+ className: 'ide-settings-checkbox',
2941
+ checked: !!(editorPrefs.smoothScrolling),
2942
+ onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { smoothScrolling: v }); }); }
2943
+ })
2944
+ ),
2945
+ React.createElement(
2946
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Hold Ctrl (or Cmd) and scroll the mouse wheel to zoom the font size' },
2947
+ React.createElement('span', { className: 'ide-settings-label' }, 'Ctrl+scroll to zoom'),
2948
+ React.createElement('input', {
2949
+ type: 'checkbox',
2950
+ className: 'ide-settings-checkbox',
2951
+ checked: !!(editorPrefs.mouseWheelZoom),
2952
+ onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { mouseWheelZoom: v }); }); }
2953
+ })
2954
+ ),
2955
+
2956
+ /* ── Behaviour ───────────────────────────────── */
2957
+ React.createElement('div', { className: 'ide-settings-section-header' }, 'Behaviour'),
2958
+ React.createElement(
2959
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'How aggressively the editor re-indents lines as you type' },
2960
+ React.createElement('span', { className: 'ide-settings-label' }, 'Auto indent'),
2961
+ React.createElement(
2962
+ 'select', {
2963
+ value: editorPrefs.autoIndent || 'full',
2964
+ onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { autoIndent: e.target.value }); }); }
2965
+ },
2966
+ React.createElement('option', { value: 'none' }, 'None (disabled)'),
2967
+ React.createElement('option', { value: 'keep' }, 'Keep current level'),
2968
+ React.createElement('option', { value: 'brackets' }, 'Indent on { and ['),
2969
+ React.createElement('option', { value: 'advanced' }, 'Language indent rules'),
2970
+ React.createElement('option', { value: 'full' }, 'Full (language grammar)')
2971
+ )
2972
+ ),
2973
+ React.createElement(
2974
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Whether pressing Enter accepts the highlighted autocomplete suggestion' },
2975
+ React.createElement('span', { className: 'ide-settings-label' }, 'Accept suggestion on Enter'),
2976
+ React.createElement(
2977
+ 'select', {
2978
+ value: editorPrefs.acceptSuggestionOnEnter || 'on',
2979
+ onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { acceptSuggestionOnEnter: e.target.value }); }); }
2980
+ },
2981
+ React.createElement('option', { value: 'on' }, 'Always'),
2982
+ React.createElement('option', { value: 'smart' }, 'Only when navigated (↑↓)'),
2983
+ React.createElement('option', { value: 'off' }, 'Never (Tab only)')
2984
+ )
2985
+ ),
2986
+ React.createElement(
2987
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Suggest completions based on words already present in open files' },
2988
+ React.createElement('span', { className: 'ide-settings-label' }, 'Word-based suggestions'),
2989
+ React.createElement(
2990
+ 'select', {
2991
+ value: editorPrefs.wordBasedSuggestions || 'matchingDocuments',
2992
+ onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { wordBasedSuggestions: e.target.value }); }); }
2993
+ },
2994
+ React.createElement('option', { value: 'off' }, 'Off'),
2995
+ React.createElement('option', { value: 'currentDocument' }, 'Current file only'),
2996
+ React.createElement('option', { value: 'matchingDocuments' }, 'Same language files'),
2997
+ React.createElement('option', { value: 'allDocuments' }, 'All open files')
2998
+ )
2999
+ ),
3000
+ React.createElement(
3001
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Auto-format pasted code using the language formatter' },
3002
+ React.createElement('span', { className: 'ide-settings-label' }, 'Format on paste'),
3003
+ React.createElement('input', {
3004
+ type: 'checkbox',
3005
+ className: 'ide-settings-checkbox',
3006
+ checked: editorPrefs.formatOnPaste !== false,
3007
+ onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { formatOnPaste: v }); }); }
3008
+ })
3009
+ ),
3010
+ React.createElement(
3011
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Re-indent and auto-close blocks as you type (e.g. after pressing Enter inside {})' },
3012
+ React.createElement('span', { className: 'ide-settings-label' }, 'Format on type'),
3013
+ React.createElement('input', {
3014
+ type: 'checkbox',
3015
+ className: 'ide-settings-checkbox',
3016
+ checked: editorPrefs.formatOnType !== false,
3017
+ onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { formatOnType: v }); }); }
3018
+ })
3019
+ ),
3020
+ React.createElement(
3021
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Show autocomplete suggestions while typing (not just on trigger characters like .)' },
3022
+ React.createElement('span', { className: 'ide-settings-label' }, 'Quick suggestions'),
3023
+ React.createElement('input', {
3024
+ type: 'checkbox',
3025
+ className: 'ide-settings-checkbox',
3026
+ checked: editorPrefs.quickSuggestions !== false,
3027
+ onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { quickSuggestions: v }); }); }
3028
+ })
3029
+ ),
2847
3030
 
2848
3031
  /* ── Formatting (Prettier) ───────────────────── */
2849
3032
  React.createElement('div', { className: 'ide-settings-section-header' }, 'Formatting'),
@@ -76,6 +76,24 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
76
76
  if (inputRef.current) inputRef.current.focus();
77
77
  };
78
78
 
79
+ // Priority tier for a file path: lower = shown first.
80
+ // Order: controller > model > helper > concern > view > job > other > noise
81
+ function getFilePriority(path) {
82
+ var p = (path || '').toLowerCase();
83
+ if (p.indexOf('/controllers/') >= 0) return 1;
84
+ if (p.indexOf('/models/') >= 0) return 2;
85
+ if (p.indexOf('/helpers/') >= 0) return 3;
86
+ if (p.indexOf('/concerns/') >= 0) return 4;
87
+ if (p.indexOf('/views/') >= 0) return 5;
88
+ if (p.indexOf('/jobs/') >= 0) return 6;
89
+ // Deprioritise: migrations, schema, compiled assets, vendor, lock files
90
+ if (p.indexOf('/migrate/') >= 0 || p.indexOf('schema.rb') >= 0) return 90;
91
+ if (p.indexOf('/public/') >= 0 || p.indexOf('/vendor/') >= 0) return 91;
92
+ if (p.slice(-7) === '.min.js' || p.slice(-8) === '.min.css' ||
93
+ p.slice(-4) === '.map' || p.slice(-5) === '.lock') return 92;
94
+ return 50;
95
+ }
96
+
79
97
  var getQuickOpenIcon = function getQuickOpenIcon(path, name, type) {
80
98
  if (type === 'dir') {
81
99
  return React.createElement('i', { className: 'fas fa-folder quick-open-result-icon quick-open-folder-icon', 'aria-hidden': 'true' });
@@ -99,7 +117,13 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
99
117
  var res = SearchService.searchFiles(query);
100
118
  // Filter by type: always include files; include dirs only when showFolders is on
101
119
  var filtered = showFolders ? res : res.filter(function(r) { return r.type !== 'dir'; });
102
- setResults(filtered.slice(0, 20));
120
+ // Stable priority sort: controller > model > helper > concern > view > job > other > noise.
121
+ // JS sort is stable in modern engines, so relative order within the same tier
122
+ // (i.e. MiniSearch relevance score) is preserved.
123
+ filtered.sort(function(a, b) {
124
+ return getFilePriority(a.path) - getFilePriority(b.path);
125
+ });
126
+ setResults(filtered.slice(0, 200));
103
127
  setSelectedIndex(0);
104
128
  }, [query, showFolders]);
105
129
 
@@ -20,6 +20,7 @@ var EditorStore = (function () {
20
20
  };
21
21
 
22
22
  var _listeners = [];
23
+ var _statusTimer = null;
23
24
 
24
25
  function getState() { return _state; }
25
26
 
@@ -42,6 +43,9 @@ var EditorStore = (function () {
42
43
  // Subscribe to changes in a specific subset of state keys.
43
44
  // The listener is only called when at least one of the watched keys changes
44
45
  // by reference (===), preventing unnecessary re-renders for unrelated updates.
46
+ // IMPORTANT: all state updates MUST produce a new object reference for any
47
+ // nested value (use Object.assign / spread — never mutate in place), otherwise
48
+ // subscribeToSlice will not detect the change.
45
49
  function subscribeToSlice(keys, fn) {
46
50
  var prev = {};
47
51
  keys.forEach(function(k) { prev[k] = _state[k]; });
@@ -57,9 +61,13 @@ var EditorStore = (function () {
57
61
  function setStatus(text, kind) {
58
62
  kind = kind || "info";
59
63
  setState({ statusMessage: { text: text, kind: kind } });
60
- // Auto-clear after 4s for non-error messages
64
+ if (_statusTimer !== null) {
65
+ clearTimeout(_statusTimer);
66
+ _statusTimer = null;
67
+ }
61
68
  if (kind !== "error") {
62
- setTimeout(function () {
69
+ _statusTimer = setTimeout(function () {
70
+ _statusTimer = null;
63
71
  if (_state.statusMessage.text === text) {
64
72
  setState({ statusMessage: { text: "", kind: "info" } });
65
73
  }