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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +132 -0
  3. data/app/assets/javascripts/mbeditor/application.js +5 -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/CombinedFileView.js +214 -0
  11. data/app/assets/javascripts/mbeditor/components/DiffViewer.js +1 -1
  12. data/app/assets/javascripts/mbeditor/components/EditorPanel.js +442 -248
  13. data/app/assets/javascripts/mbeditor/components/FileHistoryPanel.js +6 -9
  14. data/app/assets/javascripts/mbeditor/components/FileTree.js +26 -12
  15. data/app/assets/javascripts/mbeditor/components/GitPanel.js +3 -0
  16. data/app/assets/javascripts/mbeditor/components/Gutter.js +51 -0
  17. data/app/assets/javascripts/mbeditor/components/ImportConflictModal.js +60 -5
  18. data/app/assets/javascripts/mbeditor/components/LogPanel.js +3 -44
  19. data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +1241 -1256
  20. data/app/assets/javascripts/mbeditor/components/ModelGraph.js +94 -37
  21. data/app/assets/javascripts/mbeditor/components/PastePlaceModal.js +329 -0
  22. data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +47 -65
  23. data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +26 -8
  24. data/app/assets/javascripts/mbeditor/components/SettingsModal.js +342 -0
  25. data/app/assets/javascripts/mbeditor/components/ShortcutHelp.js +18 -1
  26. data/app/assets/javascripts/mbeditor/components/TabBar.js +67 -98
  27. data/app/assets/javascripts/mbeditor/editor_plugins.js +705 -323
  28. data/app/assets/javascripts/mbeditor/file_import.js +13 -15
  29. data/app/assets/javascripts/mbeditor/file_service.js +48 -39
  30. data/app/assets/javascripts/mbeditor/git_service.js +15 -1
  31. data/app/assets/javascripts/mbeditor/history_service.js +5 -13
  32. data/app/assets/javascripts/mbeditor/search_service.js +9 -0
  33. data/app/assets/javascripts/mbeditor/tab_manager.js +127 -49
  34. data/app/assets/javascripts/mbeditor/websocket_service.js +13 -5
  35. data/app/assets/stylesheets/mbeditor/application.css +6 -1
  36. data/app/assets/stylesheets/mbeditor/editor.css +672 -249
  37. data/app/assets/stylesheets/mbeditor/glass.css +174 -0
  38. data/app/assets/stylesheets/mbeditor/themes.css +93 -33
  39. data/app/channels/mbeditor/collaboration_channel.rb +17 -4
  40. data/app/controllers/mbeditor/application_controller.rb +26 -3
  41. data/app/controllers/mbeditor/editors_controller.rb +124 -439
  42. data/app/services/mbeditor/archive_service.rb +137 -0
  43. data/app/services/mbeditor/collaboration_doc_store.rb +143 -14
  44. data/app/services/mbeditor/editor_state_service.rb +14 -51
  45. data/app/services/mbeditor/file_history_service.rb +222 -0
  46. data/app/services/mbeditor/git_info_service.rb +6 -0
  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/ruby_lsp_result_translator.rb +226 -0
  52. data/app/services/mbeditor/ruby_method_ranges_service.rb +120 -0
  53. data/app/services/mbeditor/search_replace_service.rb +8 -0
  54. data/app/views/layouts/mbeditor/application.html.erb +1 -1
  55. data/lib/mbeditor/audit_log.rb +203 -0
  56. data/lib/mbeditor/configuration.rb +6 -1
  57. data/lib/mbeditor/rack/pending_migration_bypass.rb +31 -10
  58. data/lib/mbeditor/route_map.rb +5 -0
  59. data/lib/mbeditor/ruby_lsp_client.rb +82 -14
  60. data/lib/mbeditor/version.rb +1 -1
  61. data/lib/mbeditor.rb +1 -0
  62. data/public/monaco-editor/VERSIONS +1 -1
  63. data/public/monaco-editor/codicon.ttf +0 -0
  64. data/public/monaco-editor/css.worker.js +41 -37
  65. data/public/monaco-editor/editor.worker.js +19 -15
  66. data/public/monaco-editor/html.worker.js +53 -49
  67. data/public/monaco-editor/json.worker.js +36 -32
  68. data/public/monaco-editor/monaco.css +1 -1
  69. data/public/monaco-editor/monaco.js +388 -301
  70. data/public/monaco-editor/ts.worker.js +226 -222
  71. data/vendor/assets/javascripts/axios.min.js +3 -3
  72. metadata +15 -2
@@ -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
  var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
6
5
 
@@ -25,14 +24,23 @@ var useMemo = _React.useMemo;
25
24
  // the re-render happened anyway, and the quick-open index was never rebuilt.
26
25
  // JSON.stringify over the whole tree measures 0.33 ms for ~1600 nodes, well
27
26
  // under the render it saves.
28
- function _treeUpdater(newData) {
27
+ // newSignature lets a caller that has already stringified newData hand the
28
+ // result in. The tree poll needs its own changed/unchanged answer and cannot
29
+ // read one out of a functional setState updater, which React invokes during a
30
+ // later render and which has to stay pure. Without this the poll stringified
31
+ // the whole tree a second time on every tick, forever.
32
+ function _treeUpdater(newData, newSignature) {
29
33
  return function (prevData) {
30
- if (JSON.stringify(newData) === JSON.stringify(prevData)) return prevData;
34
+ var signature = newSignature != null ? newSignature : JSON.stringify(newData);
35
+ if (signature === JSON.stringify(prevData)) return prevData;
31
36
  SearchService.buildIndex(newData);
32
37
  return newData;
33
38
  };
34
39
  }
35
40
 
41
+ // The tree poll's own view of the last tree it saw.
42
+ var _lastPolledTree = null;
43
+
36
44
  var SIDEBAR_MIN_WIDTH = 280;
37
45
  var SIDEBAR_MAX_WIDTH = 560;
38
46
  var EDITOR_MIN_WIDTH = 320;
@@ -40,6 +48,7 @@ var GIT_PANEL_MIN_WIDTH = 280;
40
48
  var PANE_MIN_WIDTH_PERCENT = 20;
41
49
  var PANE_MAX_WIDTH_PERCENT = 80;
42
50
  var SIDEBAR_COLLAPSED_WIDTH = 48;
51
+ var DRAWER_MIN_HEIGHT = 120;
43
52
  // Extension -> Prettier parser. Limited to what the vendored plugins actually
44
53
  // parse (babel, estree, html, postcss, markdown); anything outside this map
45
54
  // falls through to Monaco's re-indent.
@@ -61,6 +70,27 @@ function prettierParserFor(path) {
61
70
  return PRETTIER_PARSERS[String(path || '').split('.').pop().toLowerCase()] || null;
62
71
  }
63
72
 
73
+ // The audit ring takes numbers only, so a path travels as the index of its
74
+ // extension in the legend and can never be reconstructed from the log.
75
+ function _auditExt(path) {
76
+ var audit = window.MbeditorAudit;
77
+ return audit ? audit.code('ext', String(path || '').split('.').pop().toLowerCase()) : 0;
78
+ }
79
+
80
+ // Named rather than passed an EV code so callers need no audit reference.
81
+ function _recErr(evName) {
82
+ var audit = window.MbeditorAudit;
83
+ if (audit) audit.rec(audit.EV.ERR, audit.EV[evName]);
84
+ }
85
+
86
+ // null means no formatter covers this file type, which is not a round trip.
87
+ function _recFormat(path, startedAt, formatted) {
88
+ var audit = window.MbeditorAudit;
89
+ if (audit && formatted != null) {
90
+ audit.rec(audit.EV.FORMAT, _auditExt(path), Date.now() - startedAt, formatted.length);
91
+ }
92
+ }
93
+
64
94
  // Indentation comes from the editor's own tabSize/insertSpaces, so formatting
65
95
  // produces what the user set up. `insertSpaces !== true` mirrors how
66
96
  // EditorPanel configures Monaco: anything but an explicit true means tabs.
@@ -116,7 +146,8 @@ var DEFAULT_EDITOR_PREFS = {
116
146
  wordBasedSuggestions: 'matchingDocuments',
117
147
  acceptSuggestionOnEnter: 'on',
118
148
  autoRevealInExplorer: true,
119
- toolbarIconOnly: false,
149
+ toolbarLabels: false,
150
+ glass: false,
120
151
  rubocopLintEnabled: true,
121
152
  routeHints: true,
122
153
  prettierPrintWidth: 80,
@@ -130,7 +161,8 @@ var DEFAULT_EDITOR_PREFS = {
130
161
  tabDisplayMode: 'scroll',
131
162
  persistFindState: true,
132
163
  showDotFiles: false,
133
- branchStateRestore: true
164
+ branchStateRestore: true,
165
+ auditLog: true
134
166
  };
135
167
 
136
168
  // Indentation of formatted output is the formatter's job, not a post-pass:
@@ -139,6 +171,52 @@ var DEFAULT_EDITOR_PREFS = {
139
171
  // "Convert Indentation to Tabs / to Spaces" commands (F1) cover converting a
140
172
  // file that is already open, using its own indentation guesser.
141
173
 
174
+ // Drops both rings — the browser's and the server's — so the next download is
175
+ // a trace of one session and nothing else.
176
+ function clearAuditLog() {
177
+ if (window.MbeditorAudit) window.MbeditorAudit.clear();
178
+ fetch(window.mbeditorBasePath() + '/audit_log', {
179
+ method: 'DELETE',
180
+ headers: { 'X-Mbeditor-Client': '1' }
181
+ }).then(function () {
182
+ EditorStore.setStatus('Audit log cleared', 'info');
183
+ })["catch"](function () {
184
+ EditorStore.setStatus('Could not clear the audit log', 'error');
185
+ });
186
+ }
187
+
188
+ // Flush first, so the entries recorded in this session are on the server
189
+ // before it builds the payload.
190
+ function downloadAuditLog() {
191
+ var fetchLog = function () {
192
+ return fetch(window.mbeditorBasePath() + '/audit_log').then(function (resp) {
193
+ if (!resp.ok) throw new Error('HTTP ' + resp.status);
194
+ return resp.json();
195
+ }).then(function (payload) {
196
+ // The legend lives only in the browser bundle, so it is stitched on here
197
+ // rather than uploaded — see audit_log.js.
198
+ if (payload.client && window.MbeditorAudit) payload.client.legend = window.MbeditorAudit.LEGEND;
199
+ var blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
200
+ var objectUrl = URL.createObjectURL(blob);
201
+ var link = document.createElement('a');
202
+ link.href = objectUrl;
203
+ link.download = 'mbeditor-audit-' + Date.now() + '.json';
204
+ link.rel = 'noopener';
205
+ document.body.appendChild(link);
206
+ link.click();
207
+ document.body.removeChild(link);
208
+ // Revoking in the same tick can free the blob before the browser has
209
+ // started reading it, which cancels the download instead of saving it.
210
+ setTimeout(function () { URL.revokeObjectURL(objectUrl); }, 60000);
211
+ EditorStore.setStatus('Downloading audit log...', 'info');
212
+ });
213
+ };
214
+ var flushed = window.MbeditorAudit ? window.MbeditorAudit.flush() : Promise.resolve();
215
+ flushed.then(fetchLog, fetchLog)["catch"](function (err) {
216
+ EditorStore.setStatus('Audit log download failed: ' + (err && err.message || 'network error'), 'error');
217
+ });
218
+ }
219
+
142
220
  function diffLines(oldLines, newLines) {
143
221
  var n = oldLines.length, m = newLines.length;
144
222
  var dp = [];
@@ -157,6 +235,46 @@ function diffLines(oldLines, newLines) {
157
235
  return changed;
158
236
  }
159
237
 
238
+ // Cursor line/column for the status bar.
239
+ //
240
+ // Its own component holding its own state, deliberately: the cursor moves on
241
+ // every keystroke and every click, and MbeditorApp subscribes to the whole
242
+ // store — reading the position there would re-render the entire app, tree and
243
+ // all, on each keypress. Here a move re-renders one span.
244
+ var CursorPosition = function CursorPosition() {
245
+ var _pos = React.useState(null);
246
+ var pos = _pos[0];
247
+ var setPos = _pos[1];
248
+
249
+ React.useEffect(function () {
250
+ var disposable = null;
251
+ var attach = function () {
252
+ if (disposable) { disposable.dispose(); disposable = null; }
253
+ var editor = window.__mbeditorActiveEditor;
254
+ if (!editor) { setPos(null); return; }
255
+ var read = function () {
256
+ var p = editor.getPosition();
257
+ setPos(p ? { line: p.lineNumber, col: p.column } : null);
258
+ };
259
+ read();
260
+ disposable = editor.onDidChangeCursorPosition(read);
261
+ };
262
+ attach();
263
+ window.addEventListener('mbeditor:active-editor', attach);
264
+ return function () {
265
+ window.removeEventListener('mbeditor:active-editor', attach);
266
+ if (disposable) disposable.dispose();
267
+ };
268
+ }, []);
269
+
270
+ if (!pos) return null;
271
+ return React.createElement(
272
+ 'span',
273
+ { className: 'statusbar-cursor', title: 'Line ' + pos.line + ', column ' + pos.col },
274
+ 'Ln ' + pos.line + ', Col ' + pos.col
275
+ );
276
+ };
277
+
160
278
  var SidebarActionButton = function SidebarActionButton(_ref) {
161
279
  var title = _ref.title;
162
280
  var iconClass = _ref.iconClass;
@@ -191,7 +309,13 @@ var SidebarActionButton = function SidebarActionButton(_ref) {
191
309
  className: "project-action-btn" + (danger ? " danger" : ""),
192
310
  "aria-label": ariaLabel || title,
193
311
  "aria-busy": !!ariaBusy,
194
- onClick: onClick,
312
+ onClick: function (e) {
313
+ if (onClick) onClick(e);
314
+ // Clicking leaves the button focused, which keeps the :focus-within
315
+ // tooltip open until the user clicks elsewhere. Blur to drop it
316
+ // immediately; a Tab-focused button still shows it on keyboard focus.
317
+ e.currentTarget.blur();
318
+ },
195
319
  disabled: !!disabled
196
320
  },
197
321
  !ariaBusy && React.createElement("i", { className: iconClass })
@@ -271,107 +395,126 @@ function FileReloadBanner(_ref) {
271
395
  );
272
396
  }
273
397
 
398
+ // ── Test-result cache (localStorage) ────────────────────────────────────
399
+ // One entry per test file, capped so an unbounded suite can't fill the
400
+ // origin's quota and take the unsaved-draft backup down with it (#84).
401
+ var TEST_CACHE_PREFIX = 'mbeditor_test_result_';
402
+ var TEST_CACHE_MAX_ENTRIES = 50;
403
+
404
+ // ponytail: full localStorage scan per save, fine at a 50-entry cap.
405
+ function _trimTestResultCache(maxEntries) {
406
+ var entries = [];
407
+ for (var i = 0; i < localStorage.length; i++) {
408
+ var key = localStorage.key(i);
409
+ if (key && key.indexOf(TEST_CACHE_PREFIX) === 0) {
410
+ var ts = 0;
411
+ try { ts = (JSON.parse(localStorage.getItem(key)) || {}).ts || 0; } catch (e) {}
412
+ entries.push({ key: key, ts: ts });
413
+ }
414
+ }
415
+ entries.sort(function (a, b) { return a.ts - b.ts; });
416
+ while (entries.length > maxEntries) localStorage.removeItem(entries.shift().key);
417
+ }
418
+
419
+ function saveCachedTestResult(filePath, result) {
420
+ var payload = JSON.stringify(_extends({}, result, { ts: Date.now() }));
421
+ try {
422
+ localStorage.setItem(TEST_CACHE_PREFIX + filePath, payload);
423
+ } catch (e) {
424
+ _trimTestResultCache(Math.floor(TEST_CACHE_MAX_ENTRIES / 2));
425
+ try { localStorage.setItem(TEST_CACHE_PREFIX + filePath, payload); } catch (e2) { return; }
426
+ }
427
+ _trimTestResultCache(TEST_CACHE_MAX_ENTRIES);
428
+ }
429
+
274
430
  var MbeditorApp = function MbeditorApp() {
275
431
  var _useState = useState(EditorStore.getState());
276
432
 
277
- var _useState2 = _slicedToArray(_useState, 2);
278
433
 
279
- var state = _useState2[0];
280
- var setState = _useState2[1];
434
+ var state = _useState[0];
435
+ var setState = _useState[1];
281
436
 
282
437
  var _useState21 = useState(null);
283
- var _useState22 = _slicedToArray(_useState21, 2);
284
- var historyPanelPath = _useState22[0];
285
- var setHistoryPanelPath = _useState22[1];
438
+ var historyPanelPath = _useState21[0];
439
+ var setHistoryPanelPath = _useState21[1];
286
440
 
287
441
  var _useState23 = useState(false);
288
- var _useState24 = _slicedToArray(_useState23, 2);
289
- var isNavigating = _useState24[0];
290
- var setIsNavigating = _useState24[1];
442
+ var isNavigating = _useState23[0];
443
+ var setIsNavigating = _useState23[1];
291
444
 
292
445
  var _useState25 = useState(false);
293
- var _useState26 = _slicedToArray(_useState25, 2);
294
- var isReviewOpen = _useState26[0];
295
- var setIsReviewOpen = _useState26[1];
446
+ var isReviewOpen = _useState25[0];
447
+ var setIsReviewOpen = _useState25[1];
296
448
 
297
449
  var _useState27 = useState(null);
298
- var _useState28 = _slicedToArray(_useState27, 2);
299
- var selectedCommit = _useState28[0];
300
- var setSelectedCommit = _useState28[1];
450
+ var selectedCommit = _useState27[0];
451
+ var setSelectedCommit = _useState27[1];
301
452
 
302
453
  var _useState29 = useState(null);
303
- var _useState30 = _slicedToArray(_useState29, 2);
304
- var commitDetailFiles = _useState30[0];
305
- var setCommitDetailFiles = _useState30[1];
454
+ var commitDetailFiles = _useState29[0];
455
+ var setCommitDetailFiles = _useState29[1];
456
+
457
+ var _useStateCFI = useState(0);
458
+ var conflictFileIndex = _useStateCFI[0];
459
+ var setConflictFileIndex = _useStateCFI[1];
306
460
 
307
461
  var _useState4 = useState([]);
308
462
 
309
- var _useState42 = _slicedToArray(_useState4, 2);
310
463
 
311
- var treeData = _useState42[0];
312
- var setTreeData = _useState42[1];
464
+ var treeData = _useState4[0];
465
+ var setTreeData = _useState4[1];
313
466
 
314
467
  var _useState5 = useState("");
315
468
 
316
- var _useState52 = _slicedToArray(_useState5, 2);
317
469
 
318
- var projectRootName = _useState52[0];
319
- var setProjectRootName = _useState52[1];
470
+ var projectRootName = _useState5[0];
471
+ var setProjectRootName = _useState5[1];
320
472
 
321
473
  var _useState6 = useState(null);
322
474
 
323
- var _useState62 = _slicedToArray(_useState6, 2);
324
475
 
325
- var selectedTreeNode = _useState62[0];
326
- var setSelectedTreeNode = _useState62[1];
476
+ var selectedTreeNode = _useState6[0];
477
+ var setSelectedTreeNode = _useState6[1];
327
478
 
328
479
  var _useStateSP = useState(new Set());
329
- var _useStateSP2 = _slicedToArray(_useStateSP, 2);
330
- var selectedPaths = _useStateSP2[0];
331
- var setSelectedPaths = _useStateSP2[1];
480
+ var selectedPaths = _useStateSP[0];
481
+ var setSelectedPaths = _useStateSP[1];
332
482
 
333
483
  var _useState7 = useState("");
334
484
 
335
- var _useState72 = _slicedToArray(_useState7, 2);
336
485
 
337
- var searchQuery = _useState72[0];
338
- var setSearchQuery = _useState72[1];
486
+ var searchQuery = _useState7[0];
487
+ var setSearchQuery = _useState7[1];
339
488
 
340
489
  var _useState33 = useState(false);
341
- var _useState332 = _slicedToArray(_useState33, 2);
342
- var searchLoading = _useState332[0];
343
- var setSearchLoading = _useState332[1];
490
+ var searchLoading = _useState33[0];
491
+ var setSearchLoading = _useState33[1];
344
492
 
345
493
  var searchRequestIdRef = useRef(0);
346
494
 
347
495
  var _useState33h = useState(false);
348
- var _useState33h2 = _slicedToArray(_useState33h, 2);
349
- var searchHasMore = _useState33h2[0];
350
- var setSearchHasMore = _useState33h2[1];
496
+ var searchHasMore = _useState33h[0];
497
+ var setSearchHasMore = _useState33h[1];
351
498
 
352
499
  var _useState33tc = useState(0);
353
- var _useState33tc2 = _slicedToArray(_useState33tc, 2);
354
- var searchTotalCount = _useState33tc2[0];
355
- var setSearchTotalCount = _useState33tc2[1];
500
+ var searchTotalCount = _useState33tc[0];
501
+ var setSearchTotalCount = _useState33tc[1];
356
502
 
357
503
  var searchHasMoreRef = useRef(false);
358
504
  var searchOffsetRef = useRef(0);
359
505
  var searchLoadingMoreRef = useRef(false);
360
506
 
361
507
  var _useStateRx = useState(false);
362
- var _useStateRx2 = _slicedToArray(_useStateRx, 2);
363
- var searchUseRegex = _useStateRx2[0];
364
- var setSearchUseRegex = _useStateRx2[1];
508
+ var searchUseRegex = _useStateRx[0];
509
+ var setSearchUseRegex = _useStateRx[1];
365
510
 
366
511
  var _useStateMC = useState(false);
367
- var _useStateMC2 = _slicedToArray(_useStateMC, 2);
368
- var searchMatchCase = _useStateMC2[0];
369
- var setSearchMatchCase = _useStateMC2[1];
512
+ var searchMatchCase = _useStateMC[0];
513
+ var setSearchMatchCase = _useStateMC[1];
370
514
 
371
515
  var _useStateWW = useState(false);
372
- var _useStateWW2 = _slicedToArray(_useStateWW, 2);
373
- var searchWholeWord = _useStateWW2[0];
374
- var setSearchWholeWord = _useStateWW2[1];
516
+ var searchWholeWord = _useStateWW[0];
517
+ var setSearchWholeWord = _useStateWW[1];
375
518
 
376
519
  var searchQueryRef = useRef('');
377
520
  var searchUseRegexRef = useRef(false);
@@ -395,9 +538,8 @@ var MbeditorApp = function MbeditorApp() {
395
538
  // File paths whose match list is folded away. Keyed by path, so a file that
396
539
  // scrolls out of the window keeps its state.
397
540
  var _useStateSC = useState({});
398
- var _useStateSC2 = _slicedToArray(_useStateSC, 2);
399
- var searchCollapsedFiles = _useStateSC2[0];
400
- var setSearchCollapsedFiles = _useStateSC2[1];
541
+ var searchCollapsedFiles = _useStateSC[0];
542
+ var setSearchCollapsedFiles = _useStateSC[1];
401
543
  var toggleSearchFile = function toggleSearchFile(file) {
402
544
  setSearchCollapsedFiles(function (prev) {
403
545
  var next = Object.assign({}, prev);
@@ -406,9 +548,8 @@ var MbeditorApp = function MbeditorApp() {
406
548
  });
407
549
  };
408
550
  var _useStateSV = useState({ scrollTop: 0, height: 0 });
409
- var _useStateSV2 = _slicedToArray(_useStateSV, 2);
410
- var searchViewport = _useStateSV2[0];
411
- var setSearchViewport = _useStateSV2[1];
551
+ var searchViewport = _useStateSV[0];
552
+ var setSearchViewport = _useStateSV[1];
412
553
 
413
554
  // The container's height is only known once it is on screen, and a viewport
414
555
  // of 0 would render a single row and never grow — nothing would scroll, so
@@ -441,31 +582,26 @@ var MbeditorApp = function MbeditorApp() {
441
582
  }).current;
442
583
 
443
584
  var _useStateRM = useState(false);
444
- var _useStateRM2 = _slicedToArray(_useStateRM, 2);
445
- var replaceMode = _useStateRM2[0];
446
- var setReplaceMode = _useStateRM2[1];
585
+ var replaceMode = _useStateRM[0];
586
+ var setReplaceMode = _useStateRM[1];
447
587
 
448
588
  var _useStateRQ = useState('');
449
- var _useStateRQ2 = _slicedToArray(_useStateRQ, 2);
450
- var replaceQuery = _useStateRQ2[0];
451
- var setReplaceQuery = _useStateRQ2[1];
589
+ var replaceQuery = _useStateRQ[0];
590
+ var setReplaceQuery = _useStateRQ[1];
452
591
 
453
592
  var _useStateRL = useState(false);
454
- var _useStateRL2 = _slicedToArray(_useStateRL, 2);
455
- var replaceLoading = _useStateRL2[0];
456
- var setReplaceLoading = _useStateRL2[1];
593
+ var replaceLoading = _useStateRL[0];
594
+ var setReplaceLoading = _useStateRL[1];
457
595
 
458
596
  var _useState8 = useState("explorer");
459
597
 
460
- var _useState82 = _slicedToArray(_useState8, 2);
461
598
 
462
- var activeSidebarTab = _useState82[0];
463
- var setActiveSidebarTab = _useState82[1];
599
+ var activeSidebarTab = _useState8[0];
600
+ var setActiveSidebarTab = _useState8[1];
464
601
 
465
602
  var _useStateSC = useState(false);
466
- var _useStateSC2 = _slicedToArray(_useStateSC, 2);
467
- var sidebarCollapsed = _useStateSC2[0];
468
- var setSidebarCollapsed = _useStateSC2[1];
603
+ var sidebarCollapsed = _useStateSC[0];
604
+ var setSidebarCollapsed = _useStateSC[1];
469
605
 
470
606
  // Ref mirrors for use inside long-lived event handlers registered with
471
607
  // empty-dependency effects (live search refresh on files_changed).
@@ -477,130 +613,110 @@ var MbeditorApp = function MbeditorApp() {
477
613
  var jsSyntaxCheckAvailableRef = useRef(false);
478
614
 
479
615
  var _useStateRFMap = useState({});
480
- var _useStateRFMap2 = _slicedToArray(_useStateRFMap, 2);
481
- var railsFilesMap = _useStateRFMap2[0];
482
- var setRailsFilesMap = _useStateRFMap2[1];
616
+ var railsFilesMap = _useStateRFMap[0];
617
+ var setRailsFilesMap = _useStateRFMap[1];
483
618
 
484
619
  var _useStateRFC = useState({});
485
- var _useStateRFC2 = _slicedToArray(_useStateRFC, 2);
486
- var railsGroupsCollapsed = _useStateRFC2[0];
487
- var setRailsGroupsCollapsed = _useStateRFC2[1];
620
+ var railsGroupsCollapsed = _useStateRFC[0];
621
+ var setRailsGroupsCollapsed = _useStateRFC[1];
488
622
 
489
623
  var _useStateChangelog = useState(null); // null | { content, loading, error }
490
- var _useStateChangelog2 = _slicedToArray(_useStateChangelog, 2);
491
- var changelogState = _useStateChangelog2[0];
492
- var setChangelogState = _useStateChangelog2[1];
624
+ var changelogState = _useStateChangelog[0];
625
+ var setChangelogState = _useStateChangelog[1];
493
626
 
494
627
  var _useStateSchemaModal = useState(null);
495
- var _useStateSchemaModal2 = _slicedToArray(_useStateSchemaModal, 2);
496
- var schemaModal = _useStateSchemaModal2[0];
497
- var setSchemaModal = _useStateSchemaModal2[1];
628
+ var schemaModal = _useStateSchemaModal[0];
629
+ var setSchemaModal = _useStateSchemaModal[1];
498
630
 
499
631
  var _useStateImportConflict = useState(null);
500
- var _useStateImportConflict2 = _slicedToArray(_useStateImportConflict, 2);
501
- var importConflict = _useStateImportConflict2[0];
502
- var setImportConflict = _useStateImportConflict2[1];
632
+ var importConflict = _useStateImportConflict[0];
633
+ var setImportConflict = _useStateImportConflict[1];
503
634
 
504
635
  // { initialFolder } while the upload dialog is open, null otherwise.
505
636
  var _useStateImportDialog = useState(null);
506
- var _useStateImportDialog2 = _slicedToArray(_useStateImportDialog, 2);
507
- var importDialog = _useStateImportDialog2[0];
508
- var setImportDialog = _useStateImportDialog2[1];
637
+ var importDialog = _useStateImportDialog[0];
638
+ var setImportDialog = _useStateImportDialog[1];
509
639
 
510
640
  var _useStateSchemaLoading = useState(null);
511
- var _useStateSchemaLoading2 = _slicedToArray(_useStateSchemaLoading, 2);
512
- var schemaLoadingLabel = _useStateSchemaLoading2[0];
513
- var setSchemaLoadingLabel = _useStateSchemaLoading2[1];
641
+ var schemaLoadingLabel = _useStateSchemaLoading[0];
642
+ var setSchemaLoadingLabel = _useStateSchemaLoading[1];
514
643
 
515
644
  var _useState9 = useState({});
516
645
 
517
- var _useState92 = _slicedToArray(_useState9, 2);
518
646
 
519
- var markers = _useState92[0];
520
- var setMarkers = _useState92[1];
647
+ var markers = _useState9[0];
648
+ var setMarkers = _useState9[1];
521
649
  // { tabId: [] }
522
650
 
523
651
  var _useState10 = useState({});
524
652
 
525
- var _useState102 = _slicedToArray(_useState10, 2);
526
653
 
527
- var loading = _useState102[0];
528
- var setLoading = _useState102[1];
654
+ var loading = _useState10[0];
655
+ var setLoading = _useState10[1];
529
656
 
530
657
  var _useState11 = useState(null);
531
658
 
532
- var _useState112 = _slicedToArray(_useState11, 2);
533
659
 
534
- var closingTabId = _useState112[0];
535
- var setClosingTabId = _useState112[1];
660
+ var closingTabId = _useState11[0];
661
+ var setClosingTabId = _useState11[1];
536
662
 
537
663
  var _useState12 = useState(null);
538
664
 
539
- var _useState122 = _slicedToArray(_useState12, 2);
540
665
 
541
- var closingPaneId = _useState122[0];
542
- var setClosingPaneId = _useState122[1];
666
+ var closingPaneId = _useState12[0];
667
+ var setClosingPaneId = _useState12[1];
543
668
 
544
669
  var _useState13 = useState(SIDEBAR_MIN_WIDTH);
545
670
 
546
- var _useState132 = _slicedToArray(_useState13, 2);
547
671
 
548
- var sidebarWidth = _useState132[0];
549
- var setSidebarWidth = _useState132[1];
672
+ var sidebarWidth = _useState13[0];
673
+ var setSidebarWidth = _useState13[1];
550
674
 
551
675
  var _useState14 = useState(50);
552
676
 
553
- var _useState142 = _slicedToArray(_useState14, 2);
554
677
 
555
- var pane1Width = _useState142[0];
556
- var setPane1Width = _useState142[1];
678
+ var pane1Width = _useState14[0];
679
+ var setPane1Width = _useState14[1];
557
680
  // percentage
558
681
 
559
682
  var dragSplitWidthRef = useRef(pane1Width);
560
683
 
561
684
  var _useState15 = useState(null);
562
685
 
563
- var _useState152 = _slicedToArray(_useState15, 2);
564
686
 
565
- var activeResizeMode = _useState152[0];
566
- var setActiveResizeMode = _useState152[1];
687
+ var activeResizeMode = _useState15[0];
688
+ var setActiveResizeMode = _useState15[1];
567
689
 
568
690
  var _useState16 = useState(null);
569
691
 
570
- var _useState162 = _slicedToArray(_useState16, 2);
571
692
 
572
- var draggedTab = _useState162[0];
573
- var setDraggedTab = _useState162[1];
693
+ var draggedTab = _useState16[0];
694
+ var setDraggedTab = _useState16[1];
574
695
 
575
696
  var _useState17 = useState(null);
576
697
 
577
- var _useState172 = _slicedToArray(_useState17, 2);
578
698
 
579
- var dragOverPaneId = _useState172[0];
580
- var setDragOverPaneId = _useState172[1];
699
+ var dragOverPaneId = _useState17[0];
700
+ var setDragOverPaneId = _useState17[1];
581
701
 
582
702
  var _useState18 = useState(false);
583
- var _useState182 = _slicedToArray(_useState18, 2);
584
- var showGitPanel = _useState182[0];
585
- var setShowGitPanel = _useState182[1];
703
+ var showGitPanel = _useState18[0];
704
+ var setShowGitPanel = _useState18[1];
586
705
  var showGitPanelRef = useRef(showGitPanel);
587
706
  showGitPanelRef.current = showGitPanel;
588
707
 
589
708
  var _useStateLog = useState(false);
590
- var _useStateLog2 = _slicedToArray(_useStateLog, 2);
591
- var showLogPanel = _useStateLog2[0];
592
- var setShowLogPanel = _useStateLog2[1];
709
+ var showLogPanel = _useStateLog[0];
710
+ var setShowLogPanel = _useStateLog[1];
593
711
 
594
712
  var _useStateProblems = useState(false);
595
- var _useStateProblems2 = _slicedToArray(_useStateProblems, 2);
596
- var showProblemsPanel = _useStateProblems2[0];
597
- var setShowProblemsPanel = _useStateProblems2[1];
713
+ var showProblemsPanel = _useStateProblems[0];
714
+ var setShowProblemsPanel = _useStateProblems[1];
598
715
 
599
716
  // Error/warning tallies across the open tabs, mirrored into the status bar.
600
717
  var _useStateProblemCounts = useState({ errors: 0, warnings: 0 });
601
- var _useStateProblemCounts2 = _slicedToArray(_useStateProblemCounts, 2);
602
- var problemCounts = _useStateProblemCounts2[0];
603
- var setProblemCounts = _useStateProblemCounts2[1];
718
+ var problemCounts = _useStateProblemCounts[0];
719
+ var setProblemCounts = _useStateProblemCounts[1];
604
720
 
605
721
  // Below this the toolbar's labelled buttons no longer fit beside the title
606
722
  // and the file search, and start pushing each other out of the bar.
@@ -612,9 +728,8 @@ var MbeditorApp = function MbeditorApp() {
612
728
  return typeof window.matchMedia === 'function' &&
613
729
  window.matchMedia('(max-width: ' + TOOLBAR_LABEL_MIN_WIDTH + 'px)').matches;
614
730
  });
615
- var _useStateNarrow2 = _slicedToArray(_useStateNarrow, 2);
616
- var narrowToolbar = _useStateNarrow2[0];
617
- var setNarrowToolbar = _useStateNarrow2[1];
731
+ var narrowToolbar = _useStateNarrow[0];
732
+ var setNarrowToolbar = _useStateNarrow[1];
618
733
 
619
734
  useEffect(function () {
620
735
  if (typeof window.matchMedia !== 'function') return;
@@ -634,146 +749,183 @@ var MbeditorApp = function MbeditorApp() {
634
749
  // Model graph. Built lazily — generating it eager-loads the host app — and
635
750
  // only when the Models tab is opened.
636
751
  var _useStateModelGraph = useState(null);
637
- var _useStateModelGraph2 = _slicedToArray(_useStateModelGraph, 2);
638
- var modelGraph = _useStateModelGraph2[0];
639
- var setModelGraph = _useStateModelGraph2[1];
752
+ var modelGraph = _useStateModelGraph[0];
753
+ var setModelGraph = _useStateModelGraph[1];
640
754
 
641
755
  var _useStateModelGraphLoading = useState(false);
642
- var _useStateModelGraphLoading2 = _slicedToArray(_useStateModelGraphLoading, 2);
643
- var modelGraphLoading = _useStateModelGraphLoading2[0];
644
- var setModelGraphLoading = _useStateModelGraphLoading2[1];
756
+ var modelGraphLoading = _useStateModelGraphLoading[0];
757
+ var setModelGraphLoading = _useStateModelGraphLoading[1];
645
758
 
646
759
  var _useState18g = useState(320);
647
- var _useState18g2 = _slicedToArray(_useState18g, 2);
648
- var gitPanelWidth = _useState18g2[0];
649
- var setGitPanelWidth = _useState18g2[1];
760
+ var gitPanelWidth = _useState18g[0];
761
+ var setGitPanelWidth = _useState18g[1];
650
762
  var gitPanelWidthRef = useRef(gitPanelWidth);
651
763
  gitPanelWidthRef.current = gitPanelWidth;
652
764
 
653
765
  var _useState18h = useState(false);
654
766
 
655
- var _useState18h2 = _slicedToArray(_useState18h, 2);
656
767
 
657
- var showHelp = _useState18h2[0];
658
- var setShowHelp = _useState18h2[1];
768
+ var showHelp = _useState18h[0];
769
+ var setShowHelp = _useState18h[1];
770
+
771
+ var _useState18s = useState(false);
772
+ var showSettings = _useState18s[0];
773
+ var setShowSettings = _useState18s[1];
659
774
 
660
775
  var _useStatePwa = useState(null);
661
- var _useStatePwa2 = _slicedToArray(_useStatePwa, 2);
662
- var pwaInstallPrompt = _useStatePwa2[0];
663
- var setPwaInstallPrompt = _useStatePwa2[1];
776
+ var pwaInstallPrompt = _useStatePwa[0];
777
+ var setPwaInstallPrompt = _useStatePwa[1];
664
778
 
665
779
  var _useState18b = useState(true);
666
780
 
667
- var _useState18b2 = _slicedToArray(_useState18b, 2);
668
781
 
669
- var serverOnline = _useState18b2[0];
670
- var setServerOnline = _useState18b2[1];
782
+ var serverOnline = _useState18b[0];
783
+ var setServerOnline = _useState18b[1];
784
+
785
+ // Pending migrations. file_service.js owns the truth (it is what sees the
786
+ // response header) and publishes it as a <body> class plus an event; this
787
+ // only mirrors it into the status bar.
788
+ var _useStateMig = useState(function () {
789
+ return document.body.classList.contains('mbeditor-migration-pending');
790
+ });
791
+ var migrationsPending = _useStateMig[0];
792
+ var setMigrationsPending = _useStateMig[1];
793
+
794
+ useEffect(function () {
795
+ function onChange(e) { setMigrationsPending(!!e.detail); }
796
+ window.addEventListener('mbeditor:pending-migration', onChange);
797
+ return function () { window.removeEventListener('mbeditor:pending-migration', onChange); };
798
+ }, []);
671
799
 
672
800
  var _useState18c = useState(false);
673
801
 
674
- var _useState18c2 = _slicedToArray(_useState18c, 2);
675
802
 
676
- var rubocopAvailable = _useState18c2[0];
677
- var setRubocopAvailable = _useState18c2[1];
803
+ var rubocopAvailable = _useState18c[0];
804
+ var setRubocopAvailable = _useState18c[1];
678
805
 
679
806
  var _useState18d = useState(false);
680
807
 
681
- var _useState18d2 = _slicedToArray(_useState18d, 2);
682
808
 
683
- var hamlLintAvailable = _useState18d2[0];
684
- var setHamlLintAvailable = _useState18d2[1];
809
+ var hamlLintAvailable = _useState18d[0];
810
+ var setHamlLintAvailable = _useState18d[1];
685
811
 
686
812
  var _useState18e = useState(false);
687
- var _useState18e2 = _slicedToArray(_useState18e, 2);
688
- var gitAvailable = _useState18e2[0];
689
- var setGitAvailable = _useState18e2[1];
813
+ var gitAvailable = _useState18e[0];
814
+ var setGitAvailable = _useState18e[1];
690
815
 
691
816
  var _useState18f = useState(false);
692
- var _useState18f2 = _slicedToArray(_useState18f, 2);
693
- var redmineEnabled = _useState18f2[0];
694
- var setRedmineEnabled = _useState18f2[1];
817
+ var redmineEnabled = _useState18f[0];
818
+ var setRedmineEnabled = _useState18f[1];
695
819
 
696
820
  var _useState18rc = useState(null);
697
- var _useState18rc2 = _slicedToArray(_useState18rc, 2);
698
- var rubocopConfigPath = _useState18rc2[0];
699
- var setRubocopConfigPath = _useState18rc2[1];
821
+ var rubocopConfigPath = _useState18rc[0];
822
+ var setRubocopConfigPath = _useState18rc[1];
700
823
 
701
824
  var _useState18u = useState(null);
702
- var _useState18u2 = _slicedToArray(_useState18u, 2);
703
- var testResult = _useState18u2[0];
704
- var setTestResult = _useState18u2[1];
825
+ var testResult = _useState18u[0];
826
+ var setTestResult = _useState18u[1];
705
827
 
706
828
  var _useState18v = useState(false);
707
- var _useState18v2 = _slicedToArray(_useState18v, 2);
708
- var testLoading = _useState18v2[0];
709
- var setTestLoading = _useState18v2[1];
829
+ var testLoading = _useState18v[0];
830
+ var setTestLoading = _useState18v[1];
710
831
 
711
832
  var _useState18w = useState(true);
712
- var _useState18w2 = _slicedToArray(_useState18w, 2);
713
- var testInlineVisible = _useState18w2[0];
714
- var setTestInlineVisible = _useState18w2[1];
833
+ var testInlineVisible = _useState18w[0];
834
+ var setTestInlineVisible = _useState18w[1];
715
835
 
716
836
  var _useState18x = useState(null);
717
- var _useState18x2 = _slicedToArray(_useState18x, 2);
718
- var testPanelFile = _useState18x2[0];
719
- var setTestPanelFile = _useState18x2[1];
837
+ var testPanelFile = _useState18x[0];
838
+ var setTestPanelFile = _useState18x[1];
720
839
 
721
840
  var _useState18y = useState(false);
722
- var _useState18y2 = _slicedToArray(_useState18y, 2);
723
- var testPanelOpen = _useState18y2[0];
724
- var setTestPanelOpen = _useState18y2[1];
841
+ var testPanelOpen = _useState18y[0];
842
+ var setTestPanelOpen = _useState18y[1];
725
843
 
726
844
  var _useState18p = useState(DEFAULT_EDITOR_PREFS);
727
- var _useState18p2 = _slicedToArray(_useState18p, 2);
728
- var editorPrefs = _useState18p2[0];
729
- var setEditorPrefs = _useState18p2[1];
730
-
731
- // Icon-only toolbar: on by preference, or automatically once the window is
732
- // too narrow for the labels to fit beside the title and file search.
733
- var toolbarIconOnly = editorPrefs.toolbarIconOnly || narrowToolbar;
845
+ var editorPrefs = _useState18p[0];
846
+ var setEditorPrefs = _useState18p[1];
847
+
848
+ // Icon-only toolbar: the default, since every button carries its name in a
849
+ // title attribute. Labels are opt-in, and are dropped regardless once the
850
+ // window is too narrow for them to fit beside the title and file search.
851
+ var toolbarIconOnly = !editorPrefs.toolbarLabels || narrowToolbar;
852
+
853
+ // The audit ring never reads preferences itself; enablement is pushed in.
854
+ // BOOT is recorded on the first enable rather than at mount, because the
855
+ // stored preference arrives from /state after the default has already been
856
+ // applied and switching off discards anything buffered before it.
857
+ var auditBooted = useRef(false);
858
+ useEffect(function () {
859
+ var audit = window.MbeditorAudit;
860
+ if (!audit) return;
861
+ var on = editorPrefs.auditLog !== false;
862
+ audit.setEnabled(on);
863
+ if (!on || auditBooted.current) return;
864
+ auditBooted.current = true;
865
+ // Ready is when Monaco is up and the file can be typed in, not when React
866
+ // mounted: the gap between the two is most of the boot.
867
+ Promise.resolve(window.__monacoReady).then(function () {
868
+ audit.rec(audit.EV.BOOT, performance.now() | 0);
869
+ });
870
+ }, [editorPrefs.auditLog]);
734
871
 
735
872
  var _useState19 = useState({
736
873
  openEditors: false,
737
874
  projects: false
738
875
  });
739
876
 
740
- var _useState192 = _slicedToArray(_useState19, 2);
741
877
 
742
- var collapsedSections = _useState192[0];
743
- var setCollapsedSections = _useState192[1];
878
+ var collapsedSections = _useState19[0];
879
+ var setCollapsedSections = _useState19[1];
744
880
 
745
881
  var _useState20 = useState({});
746
882
 
747
- var _useState202 = _slicedToArray(_useState20, 2);
748
883
 
749
- var expandedDirs = _useState202[0];
750
- var setExpandedDirs = _useState202[1];
884
+ var expandedDirs = _useState20[0];
885
+ var setExpandedDirs = _useState20[1];
751
886
 
752
887
  var _useState21 = useState(null);
753
888
 
754
- var _useState212 = _slicedToArray(_useState21, 2);
755
889
 
756
- var pendingCreate = _useState212[0];
757
- var setPendingCreate = _useState212[1];
890
+ var pendingCreate = _useState21[0];
891
+ var setPendingCreate = _useState21[1];
758
892
 
759
893
  var _useState22 = useState(null);
760
894
 
761
- var _useState222 = _slicedToArray(_useState22, 2);
762
895
 
763
- var pendingRename = _useState222[0];
764
- var setPendingRename = _useState222[1];
896
+ var pendingRename = _useState22[0];
897
+ var setPendingRename = _useState22[1];
765
898
 
766
899
  var _useState23 = useState(null);
767
900
 
768
- var _useState232 = _slicedToArray(_useState23, 2);
769
901
 
770
- var contextMenu = _useState232[0];
771
- var setContextMenu = _useState232[1];
902
+ var contextMenu = _useState23[0];
903
+ var setContextMenu = _useState23[1];
904
+ var contextMenuRef = useRef(null);
772
905
 
773
906
  var _useState24 = useState(140);
774
- var _useState242 = _slicedToArray(_useState24, 2);
775
- var openEditorsHeight = _useState242[0];
776
- var setOpenEditorsHeight = _useState242[1];
907
+ var openEditorsHeight = _useState24[0];
908
+ var setOpenEditorsHeight = _useState24[1];
909
+
910
+ // Both bottom drawers are resized by a gutter that sits between the editor
911
+ // card and the drawer, so the height lives here rather than in the drawer —
912
+ // a panel cannot own the handle that is no longer inside it.
913
+ var _drawerHeight = function (key) {
914
+ var saved = parseInt(window.localStorage.getItem(key), 10);
915
+ return (saved && saved >= DRAWER_MIN_HEIGHT) ? saved : 240;
916
+ };
917
+ var _useStateLogH = useState(function () { return _drawerHeight('mbeditorLogHeight'); });
918
+ var logHeight = _useStateLogH[0];
919
+ var setLogHeight = _useStateLogH[1];
920
+ var _useStateProblemsH = useState(function () { return _drawerHeight('mbeditorProblemsHeight'); });
921
+ var problemsHeight = _useStateProblemsH[0];
922
+ var setProblemsHeight = _useStateProblemsH[1];
923
+
924
+ // One-shot target for post-operation scroll reveal (create/rename/delete).
925
+ // A fresh object each time so the effect re-fires even for a repeated path.
926
+ var _useState25 = useState(null);
927
+ var revealTarget = _useState25[0];
928
+ var setRevealTarget = _useState25[1];
777
929
 
778
930
  var resizeSessionRef = useRef(null);
779
931
  var resizeRafRef = useRef(null);
@@ -783,9 +935,8 @@ var MbeditorApp = function MbeditorApp() {
783
935
  var ctrlWPendingRef = useRef(false);
784
936
  var ctrlWTimeoutRef = useRef(null);
785
937
  var _useStateCP = useState([]);
786
- var _useStateCP2 = _slicedToArray(_useStateCP, 2);
787
- var customPaths = _useStateCP2[0];
788
- var setCustomPaths = _useStateCP2[1];
938
+ var customPaths = _useStateCP[0];
939
+ var setCustomPaths = _useStateCP[1];
789
940
  var customPathsRef = useRef([]);
790
941
  customPathsRef.current = customPaths;
791
942
  // Whether to show the presence chips at all. Read at render rather than held in
@@ -798,33 +949,29 @@ var MbeditorApp = function MbeditorApp() {
798
949
  var _useStateIdent = useState(
799
950
  typeof CollaborationIdentity !== 'undefined' ? CollaborationIdentity.get() : null
800
951
  );
801
- var _useStateIdent2 = _slicedToArray(_useStateIdent, 2);
802
- var collabIdentity = _useStateIdent2[0];
803
- var setCollabIdentity = _useStateIdent2[1];
952
+ var collabIdentity = _useStateIdent[0];
953
+ var setCollabIdentity = _useStateIdent[1];
804
954
  // Presence roster: other connected participants, keyed by client_id →
805
955
  // { name, colour, current_file }. Fed by the global presence stream; rendered
806
956
  // as click-to-jump chips in the status bar.
807
957
  var _useStateRoster = useState({});
808
- var _useStateRoster2 = _slicedToArray(_useStateRoster, 2);
809
- var collabRoster = _useStateRoster2[0];
810
- var setCollabRoster = _useStateRoster2[1];
958
+ var collabRoster = _useStateRoster[0];
959
+ var setCollabRoster = _useStateRoster[1];
811
960
 
812
961
  // Follow mode (slice 8): the presence client_id of the participant whose file +
813
962
  // viewport we're tracking, or null when navigating independently. Toggled from a
814
963
  // roster chip. The file-open is driven by the effect below; the scroll-tracking
815
964
  // lives in CollaborationService.setFollow().
816
965
  var _useStateFollow = useState(null);
817
- var _useStateFollow2 = _slicedToArray(_useStateFollow, 2);
818
- var followedClientId = _useStateFollow2[0];
819
- var setFollowedClientId = _useStateFollow2[1];
966
+ var followedClientId = _useStateFollow[0];
967
+ var setFollowedClientId = _useStateFollow[1];
820
968
  var recentSavesRef = useRef({});
821
969
  var isSavingRef = useRef(false);
822
970
  // True once the saved session has finished loading into the panes. Anything
823
971
  // that opens a tab on startup must wait for this, or the restore overwrites it.
824
972
  var _useStateSR = useState(false);
825
- var _useStateSR2 = _slicedToArray(_useStateSR, 2);
826
- var sessionRestored = _useStateSR2[0];
827
- var setSessionRestored = _useStateSR2[1];
973
+ var sessionRestored = _useStateSR[0];
974
+ var setSessionRestored = _useStateSR[1];
828
975
  var pendingChangelogRef = useRef(false);
829
976
  // path -> the file's content as last seen ON DISK (newline-normalised).
830
977
  // External-change detection compares disk-to-disk; comparing disk to the
@@ -853,15 +1000,37 @@ var MbeditorApp = function MbeditorApp() {
853
1000
 
854
1001
  // ── Draft backup helpers ─────────────────────────────────────────────────
855
1002
  var draftWriteTimerRef = useRef({});
1003
+ var draftWriteWarnedRef = useRef({});
856
1004
  var serverOnlineRef = useRef(true);
857
1005
 
858
1006
  var _draftKey = function _draftKey(path) {
859
1007
  var base = typeof window.mbeditorBasePath === 'function' ? window.mbeditorBasePath() : '';
860
1008
  return 'mbeditor_draft\x00' + base + '\x00' + path;
861
1009
  };
1010
+ // localStorage is UTF-16 and most browsers cap it near 5MB, so a draft for a
1011
+ // file past this can never fit: every debounced write failed, warned once, and
1012
+ // then kept stringifying + attempting the whole buffer every 500ms of typing.
1013
+ // Skip it outright; an explicit save is the safety net for huge files.
1014
+ var MAX_DRAFT_CHARS = 1000000;
862
1015
  var _saveDraftNow = function _saveDraftNow(path, content) {
1016
+ if (typeof content === 'string' && content.length > MAX_DRAFT_CHARS) {
1017
+ if (!draftWriteWarnedRef.current[path]) {
1018
+ draftWriteWarnedRef.current[path] = true;
1019
+ EditorStore.setStatus('Crash-recovery backup skipped for ' + path + ' (file too large)', 'info');
1020
+ }
1021
+ _clearDraft(path);
1022
+ return;
1023
+ }
863
1024
  var doWrite = function() {
864
- try { localStorage.setItem(_draftKey(path), JSON.stringify({ content: content, ts: Date.now() })); } catch (e) {}
1025
+ try {
1026
+ localStorage.setItem(_draftKey(path), JSON.stringify({ content: content, ts: Date.now() }));
1027
+ } catch (e) {
1028
+ // Once per path: this fires on every debounced keystroke otherwise.
1029
+ if (!draftWriteWarnedRef.current[path]) {
1030
+ draftWriteWarnedRef.current[path] = true;
1031
+ EditorStore.setStatus('Crash-recovery backup failed for ' + path + ' (storage full)', 'error');
1032
+ }
1033
+ }
865
1034
  };
866
1035
  if (typeof requestIdleCallback !== 'undefined') {
867
1036
  requestIdleCallback(doWrite, { timeout: 2000 });
@@ -884,24 +1053,25 @@ var MbeditorApp = function MbeditorApp() {
884
1053
  };
885
1054
 
886
1055
  var _useState_dro = useState(null);
887
- var _useState_dro2 = _slicedToArray(_useState_dro, 2);
888
- var draftRestoreOffer = _useState_dro2[0];
889
- var setDraftRestoreOffer = _useState_dro2[1];
1056
+ var draftRestoreOffer = _useState_dro[0];
1057
+ var setDraftRestoreOffer = _useState_dro[1];
890
1058
 
891
1059
  var _useStateMR = useState(false);
892
- var _useStateMR2 = _slicedToArray(_useStateMR, 2);
893
- var monacoReady = _useStateMR2[0];
894
- var setMonacoReady = _useStateMR2[1];
1060
+ var monacoReady = _useStateMR[0];
1061
+ var setMonacoReady = _useStateMR[1];
895
1062
 
896
1063
  var _useStateZen = useState(false);
897
- var _useStateZen2 = _slicedToArray(_useStateZen, 2);
898
- var zenMode = _useStateZen2[0];
899
- var setZenMode = _useStateZen2[1];
1064
+ var zenMode = _useStateZen[0];
1065
+ var setZenMode = _useStateZen[1];
1066
+
1067
+ // Transient, like zen mode — never persisted, always starts closed.
1068
+ var _useStateMGV = useState(false);
1069
+ var showModelGraphView = _useStateMGV[0];
1070
+ var setShowModelGraphView = _useStateMGV[1];
900
1071
 
901
1072
  var _useStateSB = useState(false);
902
- var _useStateSB2 = _slicedToArray(_useStateSB, 2);
903
- var isSwitchingBranch = _useStateSB2[0];
904
- var setIsSwitchingBranch = _useStateSB2[1];
1073
+ var isSwitchingBranch = _useStateSB[0];
1074
+ var setIsSwitchingBranch = _useStateSB[1];
905
1075
 
906
1076
  var clamp = function clamp(value, min, max) {
907
1077
  return Math.min(max, Math.max(min, value));
@@ -955,6 +1125,48 @@ var MbeditorApp = function MbeditorApp() {
955
1125
  });
956
1126
  };
957
1127
 
1128
+ // Finds the children array of parentPath within tree ('' means the root).
1129
+ var findTreeChildren = function findTreeChildren(tree, parentPath) {
1130
+ if (!parentPath) return tree;
1131
+ for (var i = 0; i < tree.length; i++) {
1132
+ var item = tree[i];
1133
+ if (item.path === parentPath && item.type === 'folder') return item.children || [];
1134
+ if (item.type === 'folder' && item.children) {
1135
+ var found = findTreeChildren(item.children, parentPath);
1136
+ if (found) return found;
1137
+ }
1138
+ }
1139
+ return null;
1140
+ };
1141
+
1142
+ // Row to reveal after a delete: the next surviving sibling in display
1143
+ // order, else the previous one, else the parent directory. Sorted the same
1144
+ // way FileTree sorts (folders first, then name) so it matches what the
1145
+ // user actually sees.
1146
+ var computeDeleteRevealTarget = function computeDeleteRevealTarget(tree, deletedPaths) {
1147
+ var anchor = deletedPaths[0];
1148
+ var parent = parentDir(anchor);
1149
+ var siblings = findTreeChildren(tree, parent);
1150
+ if (siblings) {
1151
+ var sorted = siblings.concat().sort(function (a, b) {
1152
+ if (a.type !== b.type) return a.type === 'folder' ? -1 : 1;
1153
+ return a.name.localeCompare(b.name);
1154
+ });
1155
+ var idx = sorted.findIndex(function (n) { return n.path === anchor; });
1156
+ if (idx >= 0) {
1157
+ var deletedSet = new Set(deletedPaths);
1158
+ var j, k;
1159
+ for (j = idx + 1; j < sorted.length; j++) {
1160
+ if (!deletedSet.has(sorted[j].path)) return sorted[j].path;
1161
+ }
1162
+ for (k = idx - 1; k >= 0; k--) {
1163
+ if (!deletedSet.has(sorted[k].path)) return sorted[k].path;
1164
+ }
1165
+ }
1166
+ }
1167
+ return parent || null;
1168
+ };
1169
+
958
1170
  // Every structural mutation (create, delete, rename, import) funnels through
959
1171
  // here, so this is where the project-search cache has to be dropped. Saves
960
1172
  // invalidated it at their own call sites, which is why editing a file
@@ -1085,6 +1297,8 @@ var MbeditorApp = function MbeditorApp() {
1085
1297
  EditorStore.setStatus('Linting...', 'info');
1086
1298
  }
1087
1299
 
1300
+ var startedAt = Date.now();
1301
+
1088
1302
  // ruby-lsp answers diagnostics for Ruby files when it's available: same
1089
1303
  // markers, plus Prism syntax errors, without a per-keystroke rubocop boot.
1090
1304
  // Anything short of a usable answer falls through to the HTTP lint for
@@ -1107,6 +1321,8 @@ var MbeditorApp = function MbeditorApp() {
1107
1321
 
1108
1322
  return lintRequest.then(function (res) {
1109
1323
  var nextMarkers = res.markers || [];
1324
+ var audit = window.MbeditorAudit;
1325
+ if (audit) audit.rec(audit.EV.LINT, _auditExt(tab.path), Date.now() - startedAt, nextMarkers.length);
1110
1326
  applyMarkersForTab(tab.id, nextMarkers);
1111
1327
 
1112
1328
  if (options.showStatus) {
@@ -1116,6 +1332,7 @@ var MbeditorApp = function MbeditorApp() {
1116
1332
 
1117
1333
  return res;
1118
1334
  })["catch"](function (err) {
1335
+ _recErr('LINT');
1119
1336
  if (options.showStatus) {
1120
1337
  EditorStore.setStatus('Lint failed: ' + err.message, 'error');
1121
1338
  }
@@ -1191,9 +1408,7 @@ var MbeditorApp = function MbeditorApp() {
1191
1408
  Promise.all([FileService.getWorkspace()["catch"](function () {
1192
1409
  return null;
1193
1410
  }), refreshProjectTree()]).then(function (_ref) {
1194
- var _ref2 = _slicedToArray(_ref, 1);
1195
-
1196
- var workspace = _ref2[0];
1411
+ var workspace = _ref[0];
1197
1412
 
1198
1413
  if (workspace && workspace.rootName) {
1199
1414
  setProjectRootName(workspace.rootName);
@@ -1214,6 +1429,9 @@ var MbeditorApp = function MbeditorApp() {
1214
1429
  if (workspace && typeof workspace.rubyLspAvailable === 'boolean') {
1215
1430
  window.MBEDITOR_RUBY_LSP_AVAILABLE = workspace.rubyLspAvailable;
1216
1431
  }
1432
+ if (workspace && workspace.searchBackend) {
1433
+ window.MBEDITOR_SEARCH_BACKEND = workspace.searchBackend;
1434
+ }
1217
1435
  if (workspace && typeof workspace.gitAvailable === 'boolean') {
1218
1436
  setGitAvailable(workspace.gitAvailable);
1219
1437
  }
@@ -1231,11 +1449,15 @@ var MbeditorApp = function MbeditorApp() {
1231
1449
  // Helper: load tab content for a set of panes and restore them into EditorStore
1232
1450
  function loadPaneState(panesToLoad, focusedPaneId) {
1233
1451
  if (!panesToLoad || panesToLoad.length === 0) return Promise.resolve();
1452
+ // Settings is a modal now; a state saved when it was a tab must not
1453
+ // restore one.
1454
+ panesToLoad = panesToLoad.map(function (p) {
1455
+ return _extends({}, p, {
1456
+ tabs: p.tabs.filter(function (t) { return !t.isSettings && t.path !== '__settings__' && !t.isCombinedView; })
1457
+ });
1458
+ });
1234
1459
  var allTabs = panesToLoad.flatMap(function (p) { return p.tabs; });
1235
1460
  return Promise.all(allTabs.map(function (t) {
1236
- if (t.isSettings || t.path === '__settings__') {
1237
- return Promise.resolve({ content: '' });
1238
- }
1239
1461
  if (t.isChangelog || t.path === 'mbeditor://changelog') {
1240
1462
  return Promise.resolve({ content: '' });
1241
1463
  }
@@ -1247,9 +1469,6 @@ var MbeditorApp = function MbeditorApp() {
1247
1469
  if (t.isCombinedDiff || (t.path || '').startsWith('combined-diff://') || (t.path || '').startsWith('diff://')) {
1248
1470
  return Promise.resolve({ content: '' });
1249
1471
  }
1250
- if (t.isModelGraph || t.path === 'mbeditor://model-graph') {
1251
- return Promise.resolve({ content: '' });
1252
- }
1253
1472
  var sourcePath = t.isPreview || /::preview$/.test(t.path || '') ? t.previewFor || (t.path || '').replace(/::preview$/, '') : t.path;
1254
1473
  return FileService.getFile(sourcePath, { allowMissing: true }).then(function (data) {
1255
1474
  return {
@@ -1272,8 +1491,8 @@ var MbeditorApp = function MbeditorApp() {
1272
1491
  var tabs = [];
1273
1492
  p.tabs.forEach(function (t) {
1274
1493
  var res = results[resIdx++];
1275
- var isPlainFile = t.path && !t.isDiff && !t.isCombinedDiff && !t.isSettings &&
1276
- !t.isChangelog && !t.isPreview && !t.isModelGraph &&
1494
+ var isPlainFile = t.path && !t.isDiff && !t.isCombinedDiff &&
1495
+ !t.isChangelog && !t.isCombinedView && !t.isPreview &&
1277
1496
  !/^(diff|combined-diff):\/\//.test(t.path) && !/::preview$/.test(t.path);
1278
1497
  if (isPlainFile) {
1279
1498
  if (seenPaths[t.path]) return;
@@ -1283,9 +1502,6 @@ var MbeditorApp = function MbeditorApp() {
1283
1502
  content: res.content,
1284
1503
  externalContentVersion: (t.externalContentVersion || 0) + 1
1285
1504
  },
1286
- // A state saved before this tab type existed carries the path but
1287
- // not the flag, and would restore as a missing file.
1288
- t.path === 'mbeditor://model-graph' ? { isModelGraph: true } : {},
1289
1505
  res._isDiffResult ? { diffOriginal: res.diffOriginal, diffModified: res.diffModified } : {},
1290
1506
  typeof res.fileNotFound === 'boolean' ? { fileNotFound: res.fileNotFound, dirty: res.fileNotFound ? false : t.dirty } : {},
1291
1507
  res.image === true ? { isImage: true } : {}));
@@ -1397,10 +1613,10 @@ var MbeditorApp = function MbeditorApp() {
1397
1613
  return {
1398
1614
  id: p.id,
1399
1615
  activeTabId: p.activeTabId,
1400
- tabs: p.tabs.filter(function (t) { return !t.isCombinedDiff && !t.isModelGraph && !t.isUntitled; }).map(function (t) {
1616
+ tabs: p.tabs.filter(function (t) { return !t.isCombinedDiff && !t.isCombinedView && !t.isUntitled; }).map(function (t) {
1401
1617
  return {
1402
1618
  id: t.id, path: t.path, name: t.name, dirty: t.dirty, viewState: t.viewState,
1403
- isSettings: !!t.isSettings, isPreview: !!t.isPreview, previewFor: t.previewFor || null,
1619
+ isPreview: !!t.isPreview, previewFor: t.previewFor || null,
1404
1620
  isDiff: !!t.isDiff, diffBaseSha: t.diffBaseSha || null, diffHeadSha: t.diffHeadSha || null,
1405
1621
  repoPath: t.repoPath || null, isChangelog: !!t.isChangelog
1406
1622
  };
@@ -1483,6 +1699,7 @@ var MbeditorApp = function MbeditorApp() {
1483
1699
  if (e.key === 'Escape') {
1484
1700
  setContextMenu(null);
1485
1701
  setShowHelp(false);
1702
+ setCollabOverflowOpen(false);
1486
1703
  }
1487
1704
  };
1488
1705
 
@@ -1516,6 +1733,9 @@ var MbeditorApp = function MbeditorApp() {
1516
1733
  var reservedRight = EDITOR_MIN_WIDTH + (showGitPanelRef.current ? gitPanelWidthRef.current : 0);
1517
1734
  var maxSidebarWidth = Math.min(SIDEBAR_MAX_WIDTH, Math.max(SIDEBAR_MIN_WIDTH, rect.width - reservedRight));
1518
1735
  var nextWidth = clientX - rect.left - SIDEBAR_COLLAPSED_WIDTH;
1736
+ // Remembered for mouseup: dragged well under the minimum, the
1737
+ // explorer snaps shut instead of pinning at SIDEBAR_MIN_WIDTH.
1738
+ s.snapClose = nextWidth < SIDEBAR_MIN_WIDTH - 80;
1519
1739
  setSidebarWidth(clamp(nextWidth, SIDEBAR_MIN_WIDTH, maxSidebarWidth));
1520
1740
  }
1521
1741
 
@@ -1533,11 +1753,26 @@ var MbeditorApp = function MbeditorApp() {
1533
1753
  var nextHeight = Math.max(60, Math.min(400, s.startHeight + delta));
1534
1754
  setOpenEditorsHeight(nextHeight);
1535
1755
  }
1756
+
1757
+ // Bottom drawers. Delta-based and inverted: dragging the gutter up
1758
+ // grows the drawer. Independent of where the drawer is anchored and
1759
+ // survives a zero/unknown viewport height.
1760
+ if (s.mode === 'log' || s.mode === 'problems') {
1761
+ var vh = window.innerHeight || document.documentElement.clientHeight || 0;
1762
+ var maxH = vh > 0 ? Math.round(vh * 0.85) : Infinity;
1763
+ s.lastHeight = Math.min(maxH, Math.max(DRAWER_MIN_HEIGHT, s.startHeight + (s.startY - clientY)));
1764
+ (s.mode === 'log' ? setLogHeight : setProblemsHeight)(s.lastHeight);
1765
+ }
1536
1766
  });
1537
1767
  };
1538
1768
 
1539
1769
  var handleMouseUp = function handleMouseUp() {
1540
1770
  if (!resizeSessionRef.current) return;
1771
+ if (resizeSessionRef.current.snapClose) setSidebarCollapsed(true);
1772
+ var s = resizeSessionRef.current;
1773
+ if (s.storeKey && s.lastHeight) {
1774
+ try { window.localStorage.setItem(s.storeKey, String(s.lastHeight)); } catch (err) {}
1775
+ }
1541
1776
 
1542
1777
  if (resizeRafRef.current) {
1543
1778
  cancelAnimationFrame(resizeRafRef.current);
@@ -1850,7 +2085,10 @@ var MbeditorApp = function MbeditorApp() {
1850
2085
  function handleFileSaved(data) {
1851
2086
  var path = data && data.path;
1852
2087
  if (!path) return;
1853
- if (typeof CollaborationService === 'undefined' || !CollaborationService.isBound(path)) return;
2088
+ // Only an attached (CRDT-bound) tab has the peer's content. A room that is
2089
+ // still deferring, or degraded to local, holds its own buffer — a peer save
2090
+ // is an external change for it, handled by the files_changed path.
2091
+ if (typeof CollaborationService === 'undefined' || !CollaborationService.isAttached(path)) return;
1854
2092
 
1855
2093
  var st = EditorStore.getState();
1856
2094
  var changed = false;
@@ -1911,7 +2149,7 @@ var MbeditorApp = function MbeditorApp() {
1911
2149
  !path.startsWith('combined-diff://') &&
1912
2150
  !path.startsWith('untitled://') &&
1913
2151
  !pt.tab.isCombinedDiff &&
1914
- !pt.tab.isSettings &&
2152
+ !pt.tab.isCombinedView &&
1915
2153
  !pt.tab.isImage &&
1916
2154
  !pt.tab.isDiff &&
1917
2155
  typeof pt.tab.content === 'string';
@@ -2018,8 +2256,18 @@ var MbeditorApp = function MbeditorApp() {
2018
2256
  useEffect(function () {
2019
2257
  var intervalId = setInterval(function () {
2020
2258
  if (document.hidden) return;
2259
+ var startedAt = Date.now();
2021
2260
  FileService.getTree({ background: true }).then(function (data) {
2022
- setTreeData(_treeUpdater(data || []));
2261
+ var next = data || [];
2262
+ var audit = window.MbeditorAudit;
2263
+ // Stringified once and reused by the updater, so instrumenting the poll
2264
+ // costs no extra pass over the tree.
2265
+ var signature = audit ? JSON.stringify(next) : null;
2266
+ setTreeData(_treeUpdater(next, signature));
2267
+ if (!audit) return;
2268
+ var changed = signature !== _lastPolledTree;
2269
+ _lastPolledTree = signature;
2270
+ audit.rec(audit.EV.TREE_POLL, Date.now() - startedAt, changed ? 1 : 0);
2023
2271
  }).catch(function () {}); // silently ignore auto-refresh errors
2024
2272
  }, 10000);
2025
2273
  return function () { clearInterval(intervalId); };
@@ -2086,6 +2334,10 @@ var MbeditorApp = function MbeditorApp() {
2086
2334
  }, [monacoReady]);
2087
2335
 
2088
2336
  var handleSelectFile = function handleSelectFile(path, name, line, col, endCol) {
2337
+ // The one place every reachable "open a file" action funnels through while
2338
+ // the model graph view is up (quick open is the only such surface left —
2339
+ // explorer, search, rails and git panel are all hidden with it).
2340
+ setShowModelGraphView(false);
2089
2341
  TabManager.openTab(path, name, line, null, false, col, endCol);
2090
2342
  handleNodeSelect({ path: path, name: name || path.split('/').pop(), type: 'file' });
2091
2343
  setQuickOpen(false);
@@ -2141,6 +2393,17 @@ var MbeditorApp = function MbeditorApp() {
2141
2393
  });
2142
2394
  };
2143
2395
 
2396
+ // Drop any pending-reload prompt whose tab is no longer open in any pane.
2397
+ var _prunePendingReloads = function _prunePendingReloads() {
2398
+ EditorStore.setState({
2399
+ pendingReloads: EditorStore.getState().pendingReloads.filter(function (r) {
2400
+ return EditorStore.getState().panes.some(function (p) {
2401
+ return p.tabs.some(function (t) { return t.id === r.tabId; });
2402
+ });
2403
+ })
2404
+ });
2405
+ };
2406
+
2144
2407
  var requestCloseTab = function requestCloseTab(paneId, id) {
2145
2408
  var pane = state.panes.find(function (p) {
2146
2409
  return p.id === paneId;
@@ -2153,13 +2416,7 @@ var MbeditorApp = function MbeditorApp() {
2153
2416
  setClosingTabId(id);
2154
2417
  } else {
2155
2418
  TabManager.closeTab(paneId, id);
2156
- EditorStore.setState({
2157
- pendingReloads: EditorStore.getState().pendingReloads.filter(function (r) {
2158
- return EditorStore.getState().panes.some(function (p) {
2159
- return p.tabs.some(function (t) { return t.id === r.tabId; });
2160
- });
2161
- })
2162
- });
2419
+ _prunePendingReloads();
2163
2420
  forgetClosedTabs();
2164
2421
  }
2165
2422
  };
@@ -2178,6 +2435,7 @@ var MbeditorApp = function MbeditorApp() {
2178
2435
  }
2179
2436
 
2180
2437
  if (save) {
2438
+ tab = _freshTab(closingPaneId, tab);
2181
2439
  if (tab.isUntitled) {
2182
2440
  // Save-as converts the scratch tab to a real one (closing the scratch
2183
2441
  // tab in the process); a cancelled prompt keeps the tab open.
@@ -2191,49 +2449,20 @@ var MbeditorApp = function MbeditorApp() {
2191
2449
  });
2192
2450
  return;
2193
2451
  }
2194
- setLoading(function (prev) {
2195
- return _extends({}, prev, { save: true });
2196
- });
2197
- EditorStore.setStatus("Saving " + tab.name + "...", "info");
2198
- isSavingRef.current = true;
2199
- FileService.saveFile(tab.path, tab.content).then(function () {
2200
- noteLocalSave(tab.path, tab.content);
2452
+ _runSave(tab, function () {
2201
2453
  EditorStore.setStatus("Saved", "success");
2202
2454
  SearchService.invalidate();
2203
2455
  GitService.fetchStatusLite({ background: true });
2204
- // Reset the AVI clean baseline so undo past this save point shows dirty correctly.
2205
- var _closeEntry = window.__mbeditorModels && window.__mbeditorModels[tab.path];
2206
- if (_closeEntry && _closeEntry.model && !_closeEntry.model.isDisposed()) {
2207
- _closeEntry.cleanVersionId = _closeEntry.model.getAlternativeVersionId();
2208
- }
2209
2456
  TabManager.closeTab(closingPaneId, tab.id);
2210
- EditorStore.setState({
2211
- pendingReloads: EditorStore.getState().pendingReloads.filter(function (r) {
2212
- return EditorStore.getState().panes.some(function (p) {
2213
- return p.tabs.some(function (t) { return t.id === r.tabId; });
2214
- });
2215
- })
2216
- });
2457
+ _prunePendingReloads();
2217
2458
  forgetClosedTabs();
2218
- })["catch"](function (err) {
2219
- EditorStore.setStatus("Save failed: " + err.message, "error");
2220
- })["finally"](function () {
2221
- isSavingRef.current = false;
2222
- setLoading(function (prev) {
2223
- return _extends({}, prev, { save: false });
2224
- });
2459
+ }, function () {
2225
2460
  setClosingTabId(null);
2226
2461
  setClosingPaneId(null);
2227
2462
  });
2228
2463
  } else {
2229
2464
  TabManager.closeTab(closingPaneId, tab.id);
2230
- EditorStore.setState({
2231
- pendingReloads: EditorStore.getState().pendingReloads.filter(function (r) {
2232
- return EditorStore.getState().panes.some(function (p) {
2233
- return p.tabs.some(function (t) { return t.id === r.tabId; });
2234
- });
2235
- })
2236
- });
2465
+ _prunePendingReloads();
2237
2466
  forgetClosedTabs();
2238
2467
  setClosingTabId(null);
2239
2468
  setClosingPaneId(null);
@@ -2308,7 +2537,10 @@ var MbeditorApp = function MbeditorApp() {
2308
2537
  var input = window.prompt('Save as (path relative to workspace root):', tab.name + '.txt');
2309
2538
  if (!input || !input.trim()) return Promise.reject({ cancelled: true });
2310
2539
  var newPath = input.trim().replace(/^\/+/, '');
2540
+ var startedAt = Date.now();
2311
2541
  return FileService.saveFile(newPath, tab.content).then(function () {
2542
+ var audit = window.MbeditorAudit;
2543
+ if (audit) audit.rec(audit.EV.SAVE, _auditExt(newPath), tab.content.length, Date.now() - startedAt);
2312
2544
  noteLocalSave(newPath, tab.content);
2313
2545
  SearchService.invalidate();
2314
2546
  GitService.fetchStatusLite({ background: true });
@@ -2333,14 +2565,13 @@ var MbeditorApp = function MbeditorApp() {
2333
2565
  return {
2334
2566
  id: p.id,
2335
2567
  activeTabId: p.activeTabId,
2336
- tabs: p.tabs.filter(function(t) { return !t.isCombinedDiff && !t.isModelGraph && !t.isUntitled; }).map(function (t) {
2568
+ tabs: p.tabs.filter(function(t) { return !t.isCombinedDiff && !t.isCombinedView && !t.isUntitled; }).map(function (t) {
2337
2569
  return {
2338
2570
  id: t.id,
2339
2571
  path: t.path,
2340
2572
  name: t.name,
2341
2573
  dirty: t.dirty,
2342
2574
  viewState: t.viewState,
2343
- isSettings: !!t.isSettings,
2344
2575
  isChangelog: !!t.isChangelog,
2345
2576
  isPreview: !!t.isPreview,
2346
2577
  previewFor: t.previewFor || null,
@@ -2367,7 +2598,11 @@ var MbeditorApp = function MbeditorApp() {
2367
2598
 
2368
2599
  useEffect(function() {
2369
2600
  document.documentElement.setAttribute('data-theme', editorPrefs.theme || 'vs-dark');
2370
- }, [editorPrefs.theme]);
2601
+ // Glass is a layer over the active theme, not a theme, so it rides on its
2602
+ // own attribute and glass.css derives every colour from the theme tokens.
2603
+ if (editorPrefs.glass) document.documentElement.dataset.glass = '1';
2604
+ else delete document.documentElement.dataset.glass;
2605
+ }, [editorPrefs.theme, editorPrefs.glass]);
2371
2606
 
2372
2607
  useEffect(function() {
2373
2608
  EditorStore.setState({ editorPrefs: editorPrefs });
@@ -2491,7 +2726,7 @@ var MbeditorApp = function MbeditorApp() {
2491
2726
  });
2492
2727
  tabs.forEach(function(t) {
2493
2728
  if (Object.keys(deps).length >= RAILS_MAX_RESOURCES) return;
2494
- if (!t.path || t.path === '__settings__' || t.path.startsWith('mbeditor://')) return;
2729
+ if (!t.path || t.path.startsWith('mbeditor://')) return;
2495
2730
  var label = resourceLabelFromPath(t.path);
2496
2731
  if (label && !deps[label]) deps[label] = t.path;
2497
2732
  });
@@ -2504,7 +2739,7 @@ var MbeditorApp = function MbeditorApp() {
2504
2739
  var all = {};
2505
2740
  state.panes.forEach(function(p) {
2506
2741
  p.tabs.forEach(function(t) {
2507
- if (!t.path || t.path === '__settings__' || t.path.startsWith('mbeditor://')) return;
2742
+ if (!t.path || t.path.startsWith('mbeditor://')) return;
2508
2743
  var label = resourceLabelFromPath(t.path);
2509
2744
  if (label) all[label] = true;
2510
2745
  });
@@ -2564,14 +2799,14 @@ var MbeditorApp = function MbeditorApp() {
2564
2799
 
2565
2800
  // ── Collaboration presence (slice 7) ──────────────────────────────────────
2566
2801
  // Only real, openable files belong in presence — virtual tabs (diffs, previews,
2567
- // settings/changelog) are reported as "no file" so a peer's chip stays blank
2802
+ // changelog) are reported as "no file" so a peer's chip stays blank
2568
2803
  // rather than pointing at something click-to-jump can't open.
2569
2804
  var _presenceFileFor = function (tab) {
2570
2805
  if (!tab || !tab.path) return null;
2571
2806
  var p = tab.path;
2572
- if (tab.isDiff || tab.isCombinedDiff || tab.isCommitGraph || tab.isPreview || tab.isSettings || tab.isChangelog) return null;
2807
+ if (tab.isDiff || tab.isCombinedDiff || tab.isCommitGraph || tab.isPreview || tab.isChangelog || tab.isCombinedView) return null;
2573
2808
  if (p.indexOf('diff://') === 0 || p.indexOf('combined-diff://') === 0 || p.indexOf('mbeditor://') === 0) return null;
2574
- if (p.indexOf('::preview') !== -1 || p === '__settings__') return null;
2809
+ if (p.indexOf('::preview') !== -1) return null;
2575
2810
  return p;
2576
2811
  };
2577
2812
  var presenceFile = _presenceFileFor(activeTab);
@@ -2777,14 +3012,13 @@ var MbeditorApp = function MbeditorApp() {
2777
3012
 
2778
3013
  var collabPeerIds = Object.keys(collabRoster);
2779
3014
 
2780
- // A labelled peer chip costs ~110px (name + filename), and the titlebar button
2781
- // cluster does not shrink or wrap: past three peers it squeezes the search pill
2782
- // to its floor and then pushes Help / Install off the right edge. Drop to bare
2783
- // colour dots instead of hiding peers behind a "+N more" summary — a dot is
2784
- // ~20px, so ten peers still fit, every chip stays clickable to follow, and the
2785
- // solid/hollow ring keeps working. The name and file live in the tooltip.
2786
- var COLLAB_LABEL_LIMIT = 3;
2787
- var collabPeerLabels = !toolbarIconOnly && collabPeerIds.length <= COLLAB_LABEL_LIMIT;
3015
+ // One button per peer stopped working past ~4 people: the titlebar cluster
3016
+ // does not shrink or wrap, so it squeezed the search pill to its floor and
3017
+ // pushed Help / Install off the right edge. Fixed-width stack instead: up to
3018
+ // three overlapping avatars, the rest collapse into one "+N" with a popover.
3019
+ var COLLAB_AVATAR_LIMIT = 3;
3020
+ var collabAvatarIds = collabPeerIds.slice(0, COLLAB_AVATAR_LIMIT);
3021
+ var collabOverflowIds = collabPeerIds.slice(COLLAB_AVATAR_LIMIT);
2788
3022
 
2789
3023
  // Colour is minted from a hash before any peer is known, so it has to be
2790
3024
  // reconciled against the roster once one exists. Runs on every roster change;
@@ -2801,9 +3035,8 @@ var MbeditorApp = function MbeditorApp() {
2801
3035
  // chips sit against the right edge of the titlebar and a left-anchored card
2802
3036
  // would run off screen.
2803
3037
  var _useStateHover = useState(null);
2804
- var _useStateHover2 = _slicedToArray(_useStateHover, 2);
2805
- var collabHover = _useStateHover2[0];
2806
- var setCollabHover = _useStateHover2[1];
3038
+ var collabHover = _useStateHover[0];
3039
+ var setCollabHover = _useStateHover[1];
2807
3040
 
2808
3041
  var openCollabHover = function (cid, e) {
2809
3042
  var r = e.currentTarget.getBoundingClientRect();
@@ -2813,8 +3046,7 @@ var MbeditorApp = function MbeditorApp() {
2813
3046
  // Latency and last-seen only need to tick while the card is actually on screen,
2814
3047
  // so the interval lives and dies with it. Idle cost stays zero.
2815
3048
  var _useStateHoverTick = useState(0);
2816
- var _useStateHoverTick2 = _slicedToArray(_useStateHoverTick, 2);
2817
- var setCollabHoverTick = _useStateHoverTick2[1];
3049
+ var setCollabHoverTick = _useStateHoverTick[1];
2818
3050
  useEffect(function () {
2819
3051
  if (!collabHover) return;
2820
3052
  var id = setInterval(function () { setCollabHoverTick(function (n) { return n + 1; }); }, 1000);
@@ -2845,20 +3077,60 @@ var MbeditorApp = function MbeditorApp() {
2845
3077
  handleSelectFile(followedFile, followedFile.split('/').pop());
2846
3078
  }, [followedClientId, followedFile]);
2847
3079
 
3080
+ // Shared by the avatar stack and the overflow popover so "which peer, what
3081
+ // file, are we following them" is computed once per id instead of twice.
3082
+ var collabPeerInfo = function (cid) {
3083
+ var peer = collabRoster[cid];
3084
+ var file = peer.current_file;
3085
+ return {
3086
+ name: peer.name || 'Anonymous',
3087
+ colour: peer.colour || '#888888',
3088
+ file: file,
3089
+ following: followedClientId === cid,
3090
+ elsewhere: file !== presenceFile
3091
+ };
3092
+ };
3093
+
3094
+ // The "+N" overflow popover. A plain onMouseLeave that closes immediately
3095
+ // makes it uncrossable — the pointer has to travel off the button and onto
3096
+ // the popover — so the close is delayed and cancelled by either one's
3097
+ // onMouseEnter. Click still toggles it directly, which is what makes it
3098
+ // reachable without a mouse at all.
3099
+ // The anchor, not a boolean: the popover renders at the root next to the
3100
+ // hovercard, because an absolutely-positioned child of the title bar is
3101
+ // painted under the editor whatever its z-index.
3102
+ var _useStateOverflow = useState(null);
3103
+ var collabOverflowAnchor = _useStateOverflow[0];
3104
+ var setCollabOverflowAnchor = _useStateOverflow[1];
3105
+ var collabOverflowOpen = !!collabOverflowAnchor;
3106
+ var anchorFrom = function (el) {
3107
+ var r = el.getBoundingClientRect();
3108
+ return { top: r.bottom + 4, right: window.innerWidth - r.right };
3109
+ };
3110
+ var collabOverflowCloseTimerRef = useRef(null);
3111
+ var cancelOverflowClose = function () {
3112
+ if (collabOverflowCloseTimerRef.current) {
3113
+ clearTimeout(collabOverflowCloseTimerRef.current);
3114
+ collabOverflowCloseTimerRef.current = null;
3115
+ }
3116
+ };
3117
+ var scheduleOverflowClose = function () {
3118
+ cancelOverflowClose();
3119
+ collabOverflowCloseTimerRef.current = setTimeout(function () { setCollabOverflowAnchor(null); }, 250);
3120
+ };
3121
+
2848
3122
  // Phase 7: Per-file last-commit info shown in the status bar
2849
3123
  var _useState31 = useState(null);
2850
- var _useState32 = _slicedToArray(_useState31, 2);
2851
- var activeFileCommit = _useState32[0];
2852
- var setActiveFileCommit = _useState32[1];
3124
+ var activeFileCommit = _useState31[0];
3125
+ var setActiveFileCommit = _useState31[1];
2853
3126
 
2854
3127
  // EOL indicator — tracks current line-ending style of the active file
2855
3128
  var _useState31e = useState(null);
2856
- var _useState31e2 = _slicedToArray(_useState31e, 2);
2857
- var activeEOL = _useState31e2[0];
2858
- var setActiveEOL = _useState31e2[1];
3129
+ var activeEOL = _useState31e[0];
3130
+ var setActiveEOL = _useState31e[1];
2859
3131
 
2860
3132
  useEffect(function () {
2861
- if (!gitAvailable || !activeTab || activeTab.isDiff || activeTab.isCombinedDiff || activeTab.isCommitGraph || !activeTab.path || activeTab.path.indexOf('://') >= 0) {
3133
+ if (!gitAvailable || !activeTab || activeTab.isDiff || activeTab.isCombinedDiff || activeTab.isCombinedView || activeTab.isCommitGraph || !activeTab.path || activeTab.path.indexOf('://') >= 0) {
2862
3134
  setActiveFileCommit(null);
2863
3135
  return;
2864
3136
  }
@@ -2875,10 +3147,12 @@ var MbeditorApp = function MbeditorApp() {
2875
3147
  });
2876
3148
  }, [activeTab ? activeTab.id : null, gitAvailable]);
2877
3149
 
2878
- // Update EOL indicator whenever active tab or its content changes
3150
+ // Update EOL indicator on external loads, not on every content write. The
3151
+ // content effect fires on the 250ms typing flush, and scanning the whole
3152
+ // buffer for '\r' there was pure waste — typing cannot change the line ending.
2879
3153
  useEffect(function () {
2880
3154
  if (!activeTab || typeof activeTab.content !== 'string' ||
2881
- activeTab.isDiff || activeTab.isCombinedDiff || activeTab.isCommitGraph || activeTab.isPreview) {
3155
+ activeTab.isDiff || activeTab.isCombinedDiff || activeTab.isCombinedView || activeTab.isCommitGraph || activeTab.isPreview) {
2882
3156
  setActiveEOL(null);
2883
3157
  return;
2884
3158
  }
@@ -2889,11 +3163,11 @@ var MbeditorApp = function MbeditorApp() {
2889
3163
  } else {
2890
3164
  setActiveEOL('LF');
2891
3165
  }
2892
- }, [activeTab ? activeTab.id : null, activeTab ? activeTab.content : null]);
3166
+ }, [activeTab ? activeTab.id : null, activeTab ? activeTab.externalContentVersion : null]);
2893
3167
 
2894
3168
  useEffect(function () {
2895
3169
  if (!activeTab || typeof activeTab.content !== 'string') return;
2896
- if (activeTab.isDiff || activeTab.isCombinedDiff || activeTab.isCommitGraph) return;
3170
+ if (activeTab.isDiff || activeTab.isCombinedDiff || activeTab.isCombinedView || activeTab.isCommitGraph) return;
2897
3171
  if (isRubyPath(activeTab.path) && !rubocopAvailable) return;
2898
3172
  if (activeTab.path.endsWith('.haml') && !hamlLintAvailable) return;
2899
3173
 
@@ -2966,15 +3240,35 @@ var MbeditorApp = function MbeditorApp() {
2966
3240
  // by a formatter problem).
2967
3241
  var _formatContentForSave = function _formatContentForSave(tab) {
2968
3242
  var isRubyLang = /\.(rb|rake|gemspec)$/.test(tab.path) || /(?:^|\/)(Rakefile|Gemfile)$/.test(tab.path);
3243
+ var startedAt = Date.now();
2969
3244
  if (isRubyLang) {
2970
- if (!rubocopAvailable) return Promise.resolve(null);
2971
- return FileService.formatFile(tab.path, tab.content)
2972
- .then(function (res) { return (res && res.content) || null; })
2973
- ["catch"](function () { return null; });
3245
+ // Same route as the Format button: formatRubySource tries ruby-lsp and
3246
+ // falls back to /format's `rubocop -A`. Calling /format directly here
3247
+ // meant saving a Ruby file could produce different bytes from formatting
3248
+ // the same file by hand — the two-formatters-disagree trap this file
3249
+ // already documents for JS — and gating on RuboCop alone made
3250
+ // format-on-save silently do nothing when only ruby-lsp was present.
3251
+ if (!rubocopAvailable && !window.MBEDITOR_RUBY_LSP_AVAILABLE) return Promise.resolve(null);
3252
+ return formatRubySource(tab.path, tab.content)
3253
+ .then(function (res) {
3254
+ var out = (res && res.content) || null;
3255
+ _recFormat(tab.path, startedAt, out);
3256
+ return out;
3257
+ })
3258
+ ["catch"](function () {
3259
+ _recErr('FORMAT');
3260
+ return null;
3261
+ });
2974
3262
  }
2975
3263
  var parserName = prettierParserFor(tab.path);
2976
3264
  if (!parserName) return Promise.resolve(null);
2977
- return runPrettier(tab.content, editorPrefs, parserName)["catch"](function () { return null; });
3265
+ return runPrettier(tab.content, editorPrefs, parserName).then(function (out) {
3266
+ _recFormat(tab.path, startedAt, out);
3267
+ return out;
3268
+ })["catch"](function () {
3269
+ _recErr('FORMAT');
3270
+ return null;
3271
+ });
2978
3272
  };
2979
3273
 
2980
3274
  // Re-read a tab from the store after flushing TabManager's throttled content
@@ -3013,6 +3307,40 @@ var MbeditorApp = function MbeditorApp() {
3013
3307
  _doSave(paneId, tab);
3014
3308
  };
3015
3309
 
3310
+ // Shared save-execution flow for a known-on-disk (non-untitled) tab: the
3311
+ // saving status/loading flag, the FileService write, the AVI clean-baseline
3312
+ // reset, and save-failed/cleanup handling. `onSuccess` runs whatever the
3313
+ // caller does after a successful write (before the finally cleanup);
3314
+ // `onFinally` runs any extra caller-specific cleanup after the standard one.
3315
+ var _runSave = function _runSave(tab, onSuccess, onFinally) {
3316
+ setLoading(function (prev) {
3317
+ return _extends({}, prev, { save: true });
3318
+ });
3319
+ EditorStore.setStatus("Saving " + tab.name + "...", "info");
3320
+ isSavingRef.current = true;
3321
+ var startedAt = Date.now();
3322
+ return FileService.saveFile(tab.path, tab.content).then(function () {
3323
+ var audit = window.MbeditorAudit;
3324
+ if (audit) audit.rec(audit.EV.SAVE, _auditExt(tab.path), tab.content.length, Date.now() - startedAt);
3325
+ noteLocalSave(tab.path, tab.content);
3326
+ // Reset the AVI clean baseline so undo past this save point shows dirty correctly.
3327
+ var _modelEntry = window.__mbeditorModels && window.__mbeditorModels[tab.path];
3328
+ if (_modelEntry && _modelEntry.model && !_modelEntry.model.isDisposed()) {
3329
+ _modelEntry.cleanVersionId = _modelEntry.model.getAlternativeVersionId();
3330
+ }
3331
+ onSuccess();
3332
+ })["catch"](function (err) {
3333
+ _recErr('SAVE');
3334
+ EditorStore.setStatus("Save failed: " + err.message, "error");
3335
+ })["finally"](function () {
3336
+ isSavingRef.current = false;
3337
+ setLoading(function (prev) {
3338
+ return _extends({}, prev, { save: false });
3339
+ });
3340
+ if (onFinally) onFinally();
3341
+ });
3342
+ };
3343
+
3016
3344
  var _doSave = function _doSave(paneId, tab) {
3017
3345
  if (tab.isUntitled) {
3018
3346
  saveUntitledTab(paneId, tab)["catch"](function (err) {
@@ -3021,13 +3349,7 @@ var MbeditorApp = function MbeditorApp() {
3021
3349
  });
3022
3350
  return;
3023
3351
  }
3024
- setLoading(function (prev) {
3025
- return _extends({}, prev, { save: true });
3026
- });
3027
- EditorStore.setStatus("Saving " + tab.name + "...", "info");
3028
- isSavingRef.current = true;
3029
- FileService.saveFile(tab.path, tab.content).then(function () {
3030
- noteLocalSave(tab.path, tab.content);
3352
+ _runSave(tab, function () {
3031
3353
  var newPanes = EditorStore.getState().panes.map(function (p) {
3032
3354
  if (p.id === paneId) {
3033
3355
  return _extends({}, p, { tabs: p.tabs.map(function (t) {
@@ -3037,13 +3359,10 @@ var MbeditorApp = function MbeditorApp() {
3037
3359
  return p;
3038
3360
  });
3039
3361
  EditorStore.setState({ panes: newPanes });
3040
- // Reset the AVI clean baseline so undo past this save point shows dirty correctly.
3041
- var _modelEntry = window.__mbeditorModels && window.__mbeditorModels[tab.path];
3042
- if (_modelEntry && _modelEntry.model && !_modelEntry.model.isDisposed()) {
3043
- _modelEntry.cleanVersionId = _modelEntry.model.getAlternativeVersionId();
3044
- }
3045
3362
  // Collab: push a fresh snapshot so the server compacts the buffered deltas.
3046
- if (typeof CollaborationService !== 'undefined' && CollaborationService.isBound(tab.path)) {
3363
+ // Only for an attached tab — a deferred or degraded room pushing its local
3364
+ // buffer as a snapshot could seed a second copy into what a peer holds.
3365
+ if (typeof CollaborationService !== 'undefined' && CollaborationService.isAttached(tab.path)) {
3047
3366
  CollaborationService.pushSnapshot(tab.path);
3048
3367
  }
3049
3368
  EditorStore.setStatus("Saved", "success");
@@ -3062,8 +3381,11 @@ var MbeditorApp = function MbeditorApp() {
3062
3381
  // sprockets/react-rails pipeline can't transform (errors Monaco's TS
3063
3382
  // worker misses). Save only — never per keystroke.
3064
3383
  if (jsSyntaxCheckAvailableRef.current && /\.(js|jsx)$/i.test(tab.path)) {
3384
+ var lintStartedAt = Date.now();
3065
3385
  FileService.lintFile(tab.path, tab.content, 'javascript').then(function (res) {
3066
3386
  var babelMarkers = (res && res.markers) || [];
3387
+ var audit = window.MbeditorAudit;
3388
+ if (audit) audit.rec(audit.EV.LINT, _auditExt(tab.path), Date.now() - lintStartedAt, babelMarkers.length);
3067
3389
  if (babelMarkers.length > 0) {
3068
3390
  applyMarkersForTab(tab.id, babelMarkers);
3069
3391
  EditorStore.setStatus('Saved — babel: ' + babelMarkers[0].message, 'warning');
@@ -3073,7 +3395,9 @@ var MbeditorApp = function MbeditorApp() {
3073
3395
  // applyMarkersForTab keeps the previous map when it is already empty.
3074
3396
  applyMarkersForTab(tab.id, []);
3075
3397
  }
3076
- })["catch"](function () {});
3398
+ })["catch"](function () {
3399
+ _recErr('LINT');
3400
+ });
3077
3401
  }
3078
3402
 
3079
3403
  // The server broadcasts files_changed for this very write, and the
@@ -3084,13 +3408,6 @@ var MbeditorApp = function MbeditorApp() {
3084
3408
  // file every time does by definition, so the duplicate was the expensive
3085
3409
  // one. Without a socket no broadcast arrives, so keep it for that case.
3086
3410
  if (!_socketWillBroadcast()) GitService.fetchStatusLite({ background: true });
3087
- })["catch"](function (err) {
3088
- EditorStore.setStatus("Save failed: " + err.message, "error");
3089
- })["finally"](function () {
3090
- isSavingRef.current = false;
3091
- return setLoading(function (prev) {
3092
- return _extends({}, prev, { save: false });
3093
- });
3094
3411
  });
3095
3412
  };
3096
3413
 
@@ -3179,7 +3496,15 @@ var MbeditorApp = function MbeditorApp() {
3179
3496
  // disk stayed dirty and the only thing on offer was to hit Save All again.
3180
3497
  // Each file is now settled on its own result.
3181
3498
  var promises = dirtyTabs.map(function (tab) {
3182
- return FileService.saveFile(tab.path, tab.content);
3499
+ var startedAt = Date.now();
3500
+ return FileService.saveFile(tab.path, tab.content).then(function (res) {
3501
+ var audit = window.MbeditorAudit;
3502
+ if (audit) audit.rec(audit.EV.SAVE, _auditExt(tab.path), tab.content.length, Date.now() - startedAt);
3503
+ return res;
3504
+ }, function (err) {
3505
+ _recErr('SAVE');
3506
+ throw err;
3507
+ });
3183
3508
  });
3184
3509
  Promise.allSettled(promises).then(function (results) {
3185
3510
  var saved = dirtyTabs.filter(function (tab, i) { return results[i].status === "fulfilled"; });
@@ -3327,6 +3652,7 @@ var MbeditorApp = function MbeditorApp() {
3327
3652
  // rather than the button appearing to do nothing.
3328
3653
  var formatTabContent = function formatTabContent(tab, prefs) {
3329
3654
  prefs = prefs || editorPrefs;
3655
+ var startedAt = Date.now();
3330
3656
  if (isRubyPath(tab.path) || tab.path.endsWith('.rake')) {
3331
3657
  if (!rubocopAvailable) return Promise.reject(new Error("RuboCop is not available for this workspace."));
3332
3658
  // RuboCop's output is taken as-is. Ruby indentation belongs to the
@@ -3335,13 +3661,18 @@ var MbeditorApp = function MbeditorApp() {
3335
3661
  // the same file. The old code tried the opposite — converting the source
3336
3662
  // to tabs *before* handing it over — which RuboCop simply discarded.
3337
3663
  return formatRubySource(tab.path, tab.content).then(function (res) {
3338
- return (res && res.content) || null;
3664
+ var out = (res && res.content) || null;
3665
+ _recFormat(tab.path, startedAt, out);
3666
+ return out;
3339
3667
  });
3340
3668
  }
3341
3669
 
3342
3670
  var parserName = prettierParserFor(tab.path);
3343
3671
  if (!parserName) return Promise.resolve(null);
3344
- return runPrettier(tab.content, prefs, parserName);
3672
+ return runPrettier(tab.content, prefs, parserName).then(function (out) {
3673
+ _recFormat(tab.path, startedAt, out);
3674
+ return out;
3675
+ });
3345
3676
  };
3346
3677
 
3347
3678
  // Markers are per-model and only the mounted editor re-lints, so a document
@@ -3434,6 +3765,7 @@ var MbeditorApp = function MbeditorApp() {
3434
3765
  EditorStore.setStatus(formatted === originalContent ? "Already formatted" : "Formatted (Unsaved)", "success");
3435
3766
  GitService.fetchStatus();
3436
3767
  })["catch"](function (err) {
3768
+ _recErr('FORMAT');
3437
3769
  EditorStore.setStatus("Format failed: " + (err && err.message ? err.message : err), "error");
3438
3770
  })["finally"](function () {
3439
3771
  setLoading(function (prev) { return _extends({}, prev, { format: false }); });
@@ -3451,7 +3783,7 @@ var MbeditorApp = function MbeditorApp() {
3451
3783
  var targets = [];
3452
3784
  EditorStore.getState().panes.forEach(function (p) {
3453
3785
  p.tabs.forEach(function (t) {
3454
- if (t.path && !t.path.startsWith('mbeditor://') && t.path !== '__settings__' && typeof t.content === 'string') {
3786
+ if (t.path && !t.path.startsWith('mbeditor://') && typeof t.content === 'string') {
3455
3787
  targets.push({ paneId: p.id, tab: t });
3456
3788
  }
3457
3789
  });
@@ -3492,14 +3824,6 @@ var MbeditorApp = function MbeditorApp() {
3492
3824
  });
3493
3825
  };
3494
3826
 
3495
- var TEST_CACHE_PREFIX = 'mbeditor_test_result_';
3496
-
3497
- var saveCachedTestResult = function saveCachedTestResult(filePath, result) {
3498
- try {
3499
- localStorage.setItem(TEST_CACHE_PREFIX + filePath, JSON.stringify(result));
3500
- } catch (e) {}
3501
- };
3502
-
3503
3827
  var executeTestRun = function executeTestRun(filePath, line) {
3504
3828
  setTestLoading(true);
3505
3829
  EditorStore.setStatus(line ? 'Running test at line ' + line + '...' : 'Running tests...', 'info');
@@ -3608,6 +3932,15 @@ var MbeditorApp = function MbeditorApp() {
3608
3932
  });
3609
3933
  };
3610
3934
 
3935
+ // Paging is scroll-driven, so a list too short to scroll — few hits, or a
3936
+ // big file group just collapsed — would show "Loading more…" and never load
3937
+ // it. Fill the viewport whenever the list stops being scrollable.
3938
+ useEffect(function () {
3939
+ var el = searchResultsContainerRef.current;
3940
+ if (!searchHasMore || !el) return;
3941
+ if (el.scrollHeight <= el.clientHeight + 200) loadMoreSearchResults();
3942
+ }, [searchHasMore, state.searchResults, searchCollapsedFiles]);
3943
+
3611
3944
  var handleSearchChange = function handleSearchChange(e) {
3612
3945
  var val = e.target.value;
3613
3946
  if (!val) { clearSearch(); return; }
@@ -3850,10 +4183,8 @@ var MbeditorApp = function MbeditorApp() {
3850
4183
  if (imported > 0) {
3851
4184
  // Expand down to what was just written, so the tree actually shows it —
3852
4185
  // importing into a collapsed folder otherwise leaves the explorer looking
3853
- // untouched. Deliberately does not *select* the folder: the tree
3854
- // selection is what the toolbar's Upload button reads for its default
3855
- // destination, and pinning it here would make every later upload default
3856
- // to this import's folder.
4186
+ // untouched. Deliberately does not *select* the folder, which would move
4187
+ // the selection somewhere the user did not put it.
3857
4188
  var landed = parentDir(written[0].path);
3858
4189
  var toExpand = {};
3859
4190
  var bits = landed ? landed.split('/') : [];
@@ -3872,8 +4203,7 @@ var MbeditorApp = function MbeditorApp() {
3872
4203
  // rather than a disaster. Real targets stopPropagation before this runs.
3873
4204
  useEffect(function () {
3874
4205
  var swallow = function (e) {
3875
- var types = (e.dataTransfer && e.dataTransfer.types) || [];
3876
- if (Array.prototype.indexOf.call(types, 'Files') === -1) return;
4206
+ if (!FileImport.hasExternalFiles(e.dataTransfer)) return;
3877
4207
  e.preventDefault();
3878
4208
  // dragover is left alone beyond preventDefault: setting dropEffect here
3879
4209
  // would override the 'copy' cursor the tree sets on a valid folder.
@@ -3901,7 +4231,15 @@ var MbeditorApp = function MbeditorApp() {
3901
4231
  (entries.length === 1 ? '' : 's') + '...', 'info');
3902
4232
  }
3903
4233
 
3904
- return FileService.importFiles(FileImport.buildFormData(entries, targetFolderPath, 'ask'))
4234
+ var where = targetFolderPath || 'the workspace root';
4235
+ var noun = entries.length + ' file' + (entries.length === 1 ? '' : 's');
4236
+ var progress = function (percent) {
4237
+ EditorStore.setStatus(percent === null
4238
+ ? 'Uploaded ' + noun + ' to ' + where + ', writing them...'
4239
+ : 'Uploading ' + noun + ' to ' + where + '... ' + percent + '%', 'info');
4240
+ };
4241
+
4242
+ return FileService.importFiles(FileImport.buildFormData(entries, targetFolderPath, 'ask'), progress)
3905
4243
  .then(function(result) {
3906
4244
  if (result.conflicts && result.conflicts.length > 0) {
3907
4245
  setImportConflict({ result: result, entries: entries, targetFolderPath: targetFolderPath });
@@ -3941,32 +4279,112 @@ var MbeditorApp = function MbeditorApp() {
3941
4279
  });
3942
4280
  };
3943
4281
 
3944
- // Downloads go straight through /raw?download=1 — the browser's own save
3945
- // flow, so nothing is buffered client-side and a 5 MB file costs no memory.
3946
- // A synthetic anchor rather than location.href: navigating away from the
3947
- // editor to an attachment response leaves the page in a half-unloaded state
3948
- // in Safari.
4282
+ // /raw stays a bare anchor for the single-file case — nothing buffered
4283
+ // client-side. /archive can fail with a JSON error body a plain anchor
4284
+ // navigation can't see, so that path goes through fetch instead.
4285
+ // Right-clicking a node inside a multi-selection acts on the whole selection;
4286
+ // right-clicking outside it acts on that node alone. The standard file-manager
4287
+ // rule, and why FileTree's onContextMenu stopped collapsing the set. Every
4288
+ // menu action that can operate on more than one path asks this.
4289
+ var actsOnSelection = function actsOnSelection(node) {
4290
+ return !!(node && selectedPaths && selectedPaths.size > 1 && selectedPaths.has(node.path));
4291
+ };
4292
+
4293
+ var MAX_COMBINED_FILES = 200;
4294
+ var handleOpenCombinedView = function handleOpenCombinedView(node) {
4295
+ if (!node) return;
4296
+ var byPath = {};
4297
+ (function index(items) {
4298
+ (items || []).forEach(function (n) { byPath[n.path] = n; index(n.children); });
4299
+ })(treeData);
4300
+ var files = [];
4301
+ var seen = {};
4302
+ var walk = function walk(n) {
4303
+ if (!n) return;
4304
+ if (n.type === 'file') {
4305
+ if (!seen[n.path]) { seen[n.path] = true; files.push(n.path); }
4306
+ return;
4307
+ }
4308
+ (n.children || []).forEach(walk);
4309
+ };
4310
+ (actsOnSelection(node) ? Array.from(selectedPaths) : [node.path]).forEach(function (p) { walk(byPath[p]); });
4311
+ if (!files.length) { EditorStore.setStatus('No files to show', 'info'); return; }
4312
+ var total = files.length;
4313
+ var id = 'mbeditor://combined/' + Date.now();
4314
+ var st = EditorStore.getState();
4315
+ var pane = st.panes.find(function (p) { return p.id === st.focusedPaneId; }) || st.panes[0];
4316
+ if (!pane) return;
4317
+ var tab = { id: id, path: id, name: 'Combined (' + Math.min(total, MAX_COMBINED_FILES) + ' files)', dirty: false, content: '', isCombinedView: true, combinedPaths: files.slice(0, MAX_COMBINED_FILES), combinedTotal: total };
4318
+ EditorStore.setState({
4319
+ panes: st.panes.map(function (p) { return p.id === pane.id ? Object.assign({}, p, { tabs: p.tabs.concat(tab), activeTabId: id }) : p; }),
4320
+ focusedPaneId: pane.id
4321
+ });
4322
+ };
4323
+
3949
4324
  var handleDownloadFile = function handleDownloadFile(node) {
3950
- if (!node || node.type !== 'file') return;
3951
- var link = document.createElement('a');
3952
- link.href = window.mbeditorBasePath() + '/raw?download=1&path=' + encodeURIComponent(node.path);
3953
- link.download = node.name;
3954
- link.rel = 'noopener';
3955
- document.body.appendChild(link);
3956
- link.click();
3957
- document.body.removeChild(link);
3958
- EditorStore.setStatus('Downloading ' + node.name + '...', 'info');
4325
+ if (!node) return;
4326
+ var isMultiSelect = actsOnSelection(node);
4327
+ if (!isMultiSelect && node.type === 'file') {
4328
+ var link = document.createElement('a');
4329
+ link.href = window.mbeditorBasePath() + '/raw?download=1&path=' + encodeURIComponent(node.path);
4330
+ link.download = node.name;
4331
+ link.rel = 'noopener';
4332
+ document.body.appendChild(link);
4333
+ link.click();
4334
+ document.body.removeChild(link);
4335
+ EditorStore.setStatus('Downloading ' + node.name + '...', 'info');
4336
+ return;
4337
+ }
4338
+ var paths = isMultiSelect ? Array.from(selectedPaths) : [node.path];
4339
+ var url = window.mbeditorBasePath() + '/archive?' + paths.map(function (p) {
4340
+ return 'paths[]=' + encodeURIComponent(p);
4341
+ }).join('&');
4342
+ fetch(url).then(function (resp) {
4343
+ if (!resp.ok) {
4344
+ return resp.json().catch(function () { return {}; }).then(function (body) {
4345
+ var message = (body && body.error) || ('HTTP ' + resp.status);
4346
+ EditorStore.setStatus('Download failed: ' + message, 'error');
4347
+ });
4348
+ }
4349
+ var disposition = resp.headers.get('Content-Disposition');
4350
+ var match = disposition && disposition.match(/filename="([^"]+)"/);
4351
+ var filename = (match && match[1]) || 'archive.tar.gz';
4352
+ return resp.blob().then(function (blob) {
4353
+ var objectUrl = URL.createObjectURL(blob);
4354
+ var archiveLink = document.createElement('a');
4355
+ archiveLink.href = objectUrl;
4356
+ archiveLink.download = filename;
4357
+ archiveLink.rel = 'noopener';
4358
+ document.body.appendChild(archiveLink);
4359
+ archiveLink.click();
4360
+ document.body.removeChild(archiveLink);
4361
+ // Revoking in the same tick can free the blob before the browser has
4362
+ // started reading it, which cancels the download instead of saving it.
4363
+ setTimeout(function () { URL.revokeObjectURL(objectUrl); }, 60000);
4364
+ EditorStore.setStatus('Downloading ' + filename + '...', 'info');
4365
+ });
4366
+ }).catch(function (err) {
4367
+ EditorStore.setStatus('Download failed: ' + (err && err.message || 'network error'), 'error');
4368
+ });
4369
+ };
4370
+
4371
+ var downloadMenuLabel = function downloadMenuLabel(node) {
4372
+ if (!node) return 'Download';
4373
+ if (actsOnSelection(node)) return 'Download ' + selectedPaths.size + ' items';
4374
+ if (node.type === 'folder') return 'Download folder';
4375
+ return 'Download';
4376
+ };
4377
+
4378
+ // Deleting is irreversible, so the item has to say how much it will take —
4379
+ // "Delete" over a 3-item selection would understate it.
4380
+ var deleteMenuLabel = function deleteMenuLabel(node) {
4381
+ return actsOnSelection(node) ? 'Delete ' + selectedPaths.size + ' items' : 'Delete';
3959
4382
  };
3960
4383
 
3961
4384
  // A file node means "upload alongside this file", so the dialog opens on its
3962
4385
  // parent — right-clicking a file to upload next to it is the common gesture.
3963
4386
  var openImportDialog = function openImportDialog(node) {
3964
- var folder = '';
3965
- if (node && node.type === 'folder') {
3966
- folder = node.path;
3967
- } else if (node && node.path) {
3968
- folder = node.path.split('/').slice(0, -1).join('/');
3969
- }
4387
+ var folder = node ? (node.type === 'folder' ? node.path : parentDir(node.path)) : '';
3970
4388
  setImportDialog({ initialFolder: folder });
3971
4389
  };
3972
4390
 
@@ -3977,13 +4395,18 @@ var MbeditorApp = function MbeditorApp() {
3977
4395
 
3978
4396
  var openContextMenu = function openContextMenu(e, node) {
3979
4397
  setContextMenu({ x: e.clientX, y: e.clientY, node: node });
3980
- handleNodeSelect(node);
4398
+ if (!actsOnSelection(node)) handleNodeSelect(node);
3981
4399
  };
3982
4400
 
3983
4401
  var closeContextMenu = function closeContextMenu() {
3984
4402
  return setContextMenu(null);
3985
4403
  };
3986
4404
 
4405
+ // Clamp into view whenever the menu opens at a new position.
4406
+ useEffect(function () {
4407
+ if (contextMenu) window.clampMenuIntoView(contextMenuRef.current);
4408
+ }, [contextMenu]);
4409
+
3987
4410
  var handleContextMenuAction = function handleContextMenuAction(action) {
3988
4411
  var node = contextMenu && contextMenu.node;
3989
4412
  closeContextMenu();
@@ -4002,6 +4425,9 @@ var MbeditorApp = function MbeditorApp() {
4002
4425
  if (action === 'delete') {
4003
4426
  handleDeletePath(node);return;
4004
4427
  }
4428
+ if (action === 'combinedView' && node) {
4429
+ handleOpenCombinedView(node);return;
4430
+ }
4005
4431
  if (action === 'download' && node) {
4006
4432
  handleDownloadFile(node);return;
4007
4433
  }
@@ -4042,17 +4468,34 @@ var MbeditorApp = function MbeditorApp() {
4042
4468
  document.body.style.userSelect = 'none';
4043
4469
  };
4044
4470
 
4471
+ var startDrawerResize = function startDrawerResize(mode, startHeight, storeKey) {
4472
+ return function (e) {
4473
+ e.preventDefault();
4474
+ resizeSessionRef.current = { mode: mode, startY: e.clientY, startHeight: startHeight, storeKey: storeKey };
4475
+ setActiveResizeMode(mode);
4476
+ document.body.style.cursor = 'row-resize';
4477
+ document.body.style.userSelect = 'none';
4478
+ };
4479
+ };
4480
+
4045
4481
  var handleActivityBarClick = function handleActivityBarClick(tab) {
4046
4482
  if (tab === 'settings') {
4047
- openSettingsTab();
4483
+ setShowModelGraphView(false);
4484
+ setShowSettings(!showSettings);
4048
4485
  return;
4049
4486
  }
4050
4487
  // The model graph is a view, not a panel: it takes over the central area
4051
- // and needs the width. There is no sidebar half to show.
4488
+ // and needs the width. There is no sidebar half to show, so it toggles
4489
+ // instead of behaving like the other sidebar tabs.
4052
4490
  if (tab === 'models') {
4053
- openModelGraphTab();
4491
+ setShowModelGraphView(function (prev) {
4492
+ var next = !prev;
4493
+ if (next) loadModelGraph(false);
4494
+ return next;
4495
+ });
4054
4496
  return;
4055
4497
  }
4498
+ setShowModelGraphView(false);
4056
4499
  if (!sidebarCollapsed && activeSidebarTab === tab) {
4057
4500
  setSidebarCollapsed(true);
4058
4501
  } else {
@@ -4207,6 +4650,7 @@ var MbeditorApp = function MbeditorApp() {
4207
4650
  // Optimistically insert the new node so the tree doesn't flash empty while waiting for the server
4208
4651
  var optimisticNode = { path: path, name: name, type: type === 'folder' ? 'folder' : 'file', children: type === 'folder' ? [] : undefined };
4209
4652
  setTreeData(function (prev) { return insertNodeIntoTree(prev, parentPath, optimisticNode); });
4653
+ setRevealTarget({ path: path });
4210
4654
 
4211
4655
  if (type === 'file') {
4212
4656
  setLoading(function (prev) {
@@ -4337,6 +4781,7 @@ var MbeditorApp = function MbeditorApp() {
4337
4781
  EditorStore.setStatus('Renamed to: ' + renamedPath, 'success');
4338
4782
  return refreshProjectTree().then(function () {
4339
4783
  GitService.fetchStatusLite({ background: true });
4784
+ setRevealTarget({ path: renamedPath });
4340
4785
  });
4341
4786
  })["catch"](function (err) {
4342
4787
  var message = err && err.response && err.response.data && err.response.data.error || err.message;
@@ -4353,10 +4798,12 @@ var MbeditorApp = function MbeditorApp() {
4353
4798
  };
4354
4799
 
4355
4800
  var handleDeletePath = function handleDeletePath(targetNode) {
4356
- // If a specific node is provided (e.g. from context menu), delete just that.
4357
- // Otherwise delete all paths in the multi-selection (or fall back to selectedTreeNode).
4801
+ // A node from the context menu deletes just that node — unless it is part
4802
+ // of a live multi-selection, in which case the whole selection goes, the
4803
+ // same rule Download follows. With no node (the toolbar button) it is
4804
+ // always the selection, falling back to selectedTreeNode.
4358
4805
  var pathsToDelete;
4359
- if (targetNode !== undefined && targetNode) {
4806
+ if (targetNode && !actsOnSelection(targetNode)) {
4360
4807
  pathsToDelete = [targetNode.path];
4361
4808
  } else if (selectedPaths && selectedPaths.size > 0) {
4362
4809
  pathsToDelete = Array.from(selectedPaths);
@@ -4380,6 +4827,10 @@ var MbeditorApp = function MbeditorApp() {
4380
4827
  var confirmed = window.confirm('Delete ' + label + '? This cannot be undone.');
4381
4828
  if (!confirmed) return;
4382
4829
 
4830
+ // Computed against the pre-delete tree, since the deleted paths' siblings
4831
+ // are only known before the tree is refreshed.
4832
+ var deleteRevealTarget = computeDeleteRevealTarget(treeData, pathsToDelete);
4833
+
4383
4834
  setLoading(function (prev) {
4384
4835
  return _extends({}, prev, { deletePath: true });
4385
4836
  });
@@ -4398,6 +4849,7 @@ var MbeditorApp = function MbeditorApp() {
4398
4849
  }
4399
4850
  return refreshProjectTree().then(function () {
4400
4851
  GitService.fetchStatusLite({ background: true });
4852
+ if (failures.length === 0 && deleteRevealTarget) setRevealTarget({ path: deleteRevealTarget });
4401
4853
  });
4402
4854
  })["finally"](function () {
4403
4855
  setLoading(function (prev) {
@@ -4415,6 +4867,35 @@ var MbeditorApp = function MbeditorApp() {
4415
4867
  var canLintAndFormat = !!activeTab;
4416
4868
  var hasGitBranch = !!(state.gitBranch && state.gitBranch.trim());
4417
4869
 
4870
+ // Derived at render from state already in the store — not stored via setState,
4871
+ // so the 5s git-status poll (MbeditorApp.js re-render rules) never fires an
4872
+ // extra re-render on an unchanged tick. Only conflictFileIndex is state, and
4873
+ // it changes only on explicit user click, never from a poll.
4874
+ var conflictedFiles = (state.gitInfo && state.gitInfo.workingTree || []).filter(function (f) {
4875
+ return window.MBEDITOR_UNMERGED_STATUSES[(f.status || '').trim()];
4876
+ });
4877
+
4878
+ // Opens the conflicted file at index idx (wrapping), landing the cursor/reveal
4879
+ // on its first `<<<<<<<` marker. Prefers an already-open tab's live content
4880
+ // (may hold unsaved edits) over a fresh disk fetch.
4881
+ var openConflictedFileAt = function openConflictedFileAt(idx) {
4882
+ if (!conflictedFiles.length) return;
4883
+ var i = ((idx % conflictedFiles.length) + conflictedFiles.length) % conflictedFiles.length;
4884
+ var file = conflictedFiles[i];
4885
+ setConflictFileIndex(i);
4886
+ var openTab = state.panes.reduce(function (found, p) {
4887
+ return found || p.tabs.find(function (t) { return t.path === file.path; });
4888
+ }, null);
4889
+ var name = file.path.split('/').pop();
4890
+ var contentPromise = openTab ? Promise.resolve({ content: openTab.content }) : FileService.getFile(file.path);
4891
+ contentPromise.then(function (data) {
4892
+ var content = typeof data.content === 'string' ? data.content : '';
4893
+ var blocks = ConflictParser.parse(content);
4894
+ var line = blocks.length ? blocks[0].startLine + 1 : null;
4895
+ TabManager.openTab(file.path, name, line);
4896
+ });
4897
+ };
4898
+
4418
4899
  var renderTabBar = function renderTabBar(paneId, tabs, activeId) {
4419
4900
  return React.createElement(TabBar, {
4420
4901
  tabs: tabs,
@@ -4427,7 +4908,7 @@ var MbeditorApp = function MbeditorApp() {
4427
4908
  onSelect: function (id) {
4428
4909
  // Sync explorer selection with the newly active tab so there's only one highlight
4429
4910
  var tab = tabs.find(function(t) { return t.id === id; });
4430
- if (tab && tab.path && !tab.path.startsWith('mbeditor://') && tab.path !== '__settings__') {
4911
+ if (tab && tab.path && !tab.path.startsWith('mbeditor://')) {
4431
4912
  handleNodeSelect({ path: tab.path, name: tab.name, type: 'file' });
4432
4913
  }
4433
4914
  TabManager.switchTab(paneId, id);
@@ -4464,78 +4945,6 @@ var MbeditorApp = function MbeditorApp() {
4464
4945
  });
4465
4946
  };
4466
4947
 
4467
- function openSettingsTab() {
4468
- var st = EditorStore.getState();
4469
- var foundPaneId = null;
4470
- var foundTab = null;
4471
- st.panes.forEach(function(p) {
4472
- if (!foundTab) {
4473
- var t = p.tabs.find(function(tab) { return tab.path === '__settings__'; });
4474
- if (t) { foundTab = t; foundPaneId = p.id; }
4475
- }
4476
- });
4477
- if (foundTab) {
4478
- var newPanes = st.panes.map(function(p) {
4479
- if (p.id === foundPaneId) return Object.assign({}, p, { activeTabId: '__settings__' });
4480
- return p;
4481
- });
4482
- EditorStore.setState({ panes: newPanes, focusedPaneId: foundPaneId, activeTabId: '__settings__' });
4483
- return;
4484
- }
4485
- var paneId = st.focusedPaneId;
4486
- var pane = st.panes.find(function(p) { return p.id === paneId; }) || st.panes[0];
4487
- if (!pane) return;
4488
- paneId = pane.id;
4489
- var newTab = { id: '__settings__', path: '__settings__', name: 'Settings', dirty: false, content: '', isSettings: true };
4490
- var newPanes2 = st.panes.map(function(p) {
4491
- if (p.id === paneId) return Object.assign({}, p, { tabs: p.tabs.concat(newTab), activeTabId: '__settings__' });
4492
- return p;
4493
- });
4494
- EditorStore.setState({ panes: newPanes2, focusedPaneId: paneId, activeTabId: '__settings__' });
4495
- }
4496
-
4497
- // The diagram lives in an editor tab, not the sidebar: a layered graph is
4498
- // inherently wide and a ~300px panel can only ever show its first column.
4499
- // The sidebar tab is the entry point and the searchable model list.
4500
- var MODEL_GRAPH_TAB_ID = 'mbeditor://model-graph';
4501
- function openModelGraphTab() {
4502
- var st = EditorStore.getState();
4503
- var paneId = st.focusedPaneId;
4504
-
4505
- var existing = null;
4506
- st.panes.forEach(function (p) {
4507
- if (!existing && p.tabs.some(function (t) { return t.id === MODEL_GRAPH_TAB_ID; })) {
4508
- existing = p.id;
4509
- }
4510
- });
4511
- if (existing) {
4512
- EditorStore.setState({
4513
- panes: st.panes.map(function (p) {
4514
- return p.id === existing ? Object.assign({}, p, { activeTabId: MODEL_GRAPH_TAB_ID }) : p;
4515
- }),
4516
- focusedPaneId: existing
4517
- });
4518
- return;
4519
- }
4520
-
4521
- var pane = st.panes.find(function (p) { return p.id === paneId; }) || st.panes[0];
4522
- if (!pane) return;
4523
-
4524
- var newTab = {
4525
- id: MODEL_GRAPH_TAB_ID, path: MODEL_GRAPH_TAB_ID, name: 'Model Graph',
4526
- dirty: false, content: '', isModelGraph: true
4527
- };
4528
- EditorStore.setState({
4529
- panes: st.panes.map(function (p) {
4530
- return p.id === pane.id
4531
- ? Object.assign({}, p, { tabs: p.tabs.concat(newTab), activeTabId: MODEL_GRAPH_TAB_ID })
4532
- : p;
4533
- }),
4534
- focusedPaneId: pane.id
4535
- });
4536
- loadModelGraph(false);
4537
- }
4538
-
4539
4948
  var CHANGELOG_TAB_ID = 'mbeditor://changelog';
4540
4949
  function openChangelogTab() {
4541
4950
  var st = EditorStore.getState();
@@ -4593,10 +5002,11 @@ var MbeditorApp = function MbeditorApp() {
4593
5002
  { className: "ide-titlebar" },
4594
5003
  React.createElement("i", { className: "fas fa-layer-group ide-titlebar-icon" }),
4595
5004
  React.createElement(
5005
+ // The product name, not the window title: the host and port are in the
5006
+ // browser's own address bar, and the long form crowded the search pill.
4596
5007
  "div",
4597
- { className: "ide-titlebar-title" },
4598
- "Mini Browser Editor — ",
4599
- window.location.host
5008
+ { className: "ide-titlebar-title", title: "Mini Browser Editor — " + window.location.host },
5009
+ "Mbeditor"
4600
5010
  ),
4601
5011
  // The slot claims all the room between the title and the buttons; the
4602
5012
  // search pill then takes 75% of it, centred. Sizing the pill against a
@@ -4686,13 +5096,7 @@ var MbeditorApp = function MbeditorApp() {
4686
5096
  !toolbarIconOnly && " Git"
4687
5097
  )
4688
5098
  ),
4689
- // Collaboration trouble chip. Deliberately not shown when everything is
4690
- // healthy and you are simply alone — that was the whole point of hiding
4691
- // the solo chip. It appears only when a condition pairing needs has
4692
- // actually failed, so silence means "nobody here" and never "quietly
4693
- // broken", which is exactly the ambiguity that made a real pairing
4694
- // failure impossible to diagnose from the other machine.
4695
- collabTrouble && React.createElement(
5099
+ conflictedFiles.length > 0 && React.createElement(
4696
5100
  React.Fragment,
4697
5101
  null,
4698
5102
  React.createElement("div", { className: "statusbar-sep" }),
@@ -4700,11 +5104,46 @@ var MbeditorApp = function MbeditorApp() {
4700
5104
  "button",
4701
5105
  {
4702
5106
  type: "button",
4703
- className: "statusbar-btn mbeditor-collab-trouble",
4704
- title: "Pairing is not available — click for details",
4705
- onClick: function () { setCollabDiagOpen(true); }
5107
+ className: "statusbar-btn mbeditor-conflict-chip",
5108
+ title: "Open the first of " + conflictedFiles.length + " conflicted file" + (conflictedFiles.length === 1 ? "" : "s"),
5109
+ onClick: function () { openConflictedFileAt(0); }
4706
5110
  },
4707
- React.createElement("i", { className: "fas fa-user-slash" }),
5111
+ React.createElement("i", { className: "fas fa-exclamation-circle" }),
5112
+ // The count always renders, even icon-only: it is the chip's
5113
+ // content, not a label. A bare warning icon says less than nothing.
5114
+ " " + conflictedFiles.length,
5115
+ !toolbarIconOnly && (conflictedFiles.length === 1 ? " conflict" : " conflicts")
5116
+ ),
5117
+ conflictedFiles.length > 1 && React.createElement(
5118
+ "button",
5119
+ {
5120
+ type: "button",
5121
+ className: "statusbar-btn",
5122
+ title: "Open the next conflicted file",
5123
+ onClick: function () { openConflictedFileAt(conflictFileIndex + 1); }
5124
+ },
5125
+ React.createElement("i", { className: "fas fa-arrow-right" })
5126
+ )
5127
+ ),
5128
+ // Collaboration trouble chip. Deliberately not shown when everything is
5129
+ // healthy and you are simply alone — that was the whole point of hiding
5130
+ // the solo chip. It appears only when a condition pairing needs has
5131
+ // actually failed, so silence means "nobody here" and never "quietly
5132
+ // broken", which is exactly the ambiguity that made a real pairing
5133
+ // failure impossible to diagnose from the other machine.
5134
+ collabTrouble && React.createElement(
5135
+ React.Fragment,
5136
+ null,
5137
+ React.createElement("div", { className: "statusbar-sep" }),
5138
+ React.createElement(
5139
+ "button",
5140
+ {
5141
+ type: "button",
5142
+ className: "statusbar-btn mbeditor-collab-trouble",
5143
+ title: "Pairing is not available — click for details",
5144
+ onClick: function () { setCollabDiagOpen(true); }
5145
+ },
5146
+ React.createElement("i", { className: "fas fa-user-slash" }),
4708
5147
  !toolbarIconOnly && " Pairing off"
4709
5148
  )
4710
5149
  ),
@@ -4737,45 +5176,48 @@ var MbeditorApp = function MbeditorApp() {
4737
5176
  React.Fragment,
4738
5177
  null,
4739
5178
  React.createElement("div", { className: "statusbar-sep" }),
4740
- collabPeerIds.map(function (cid) {
4741
- var peer = collabRoster[cid];
4742
- var file = peer.current_file;
4743
- var name = peer.name || 'Anonymous';
4744
- var colour = peer.colour || '#888888';
4745
- var following = followedClientId === cid;
4746
- // Solid dot: they are in the file you are looking at, so their caret
4747
- // is on screen. Hollow ring: they are somewhere else and there is
4748
- // nothing to see — without this the chip looked identical either way
4749
- // and a peer's caret just vanished with no explanation.
4750
- var elsewhere = file !== presenceFile;
4751
- return React.createElement(
4752
- "button",
4753
- {
4754
- key: cid,
4755
- type: "button",
4756
- className: "statusbar-btn",
4757
- style: following
4758
- ? { background: 'color-mix(in srgb, ' + colour + ' 28%, transparent)' }
4759
- : undefined,
4760
- onMouseEnter: function (e) { openCollabHover(cid, e); },
4761
- onMouseLeave: function () { setCollabHover(null); },
4762
- onClick: function () { toggleFollow(cid); }
4763
- },
4764
- React.createElement("i", {
4765
- className: (following ? "fas fa-eye" : (elsewhere ? "far fa-circle" : "fas fa-circle")) +
4766
- " collab-pulse",
4767
- style: { color: colour, fontSize: "0.7em", marginRight: "2px" }
4768
- }),
4769
- collabPeerLabels && (" " + name),
4770
- // Where they went, when they are not where you are. Basename only —
4771
- // the chip has ~110px to spend and the full path is in the tooltip.
4772
- collabPeerLabels && elsewhere && file && React.createElement(
4773
- "span",
4774
- { style: { opacity: 0.65, marginLeft: "4px" } },
4775
- file.split('/').pop()
5179
+ React.createElement(
5180
+ "div",
5181
+ { className: "collab-avatar-stack" },
5182
+ collabAvatarIds.map(function (cid) {
5183
+ var info = collabPeerInfo(cid);
5184
+ return React.createElement(
5185
+ "button",
5186
+ {
5187
+ key: cid,
5188
+ type: "button",
5189
+ className: "collab-avatar" +
5190
+ (info.following ? " collab-avatar-following" : "") +
5191
+ (info.elsewhere ? " collab-avatar-elsewhere" : ""),
5192
+ style: { background: info.colour },
5193
+ // The dot carries no glyph, so the name has to reach a
5194
+ // screen reader some other way.
5195
+ "aria-label": info.name,
5196
+ onMouseEnter: function (e) { openCollabHover(cid, e); },
5197
+ onMouseLeave: function () { setCollabHover(null); },
5198
+ onClick: function () { toggleFollow(cid); }
5199
+ }
5200
+ );
5201
+ }),
5202
+ collabOverflowIds.length > 0 && React.createElement(
5203
+ "div",
5204
+ { className: "collab-avatar-overflow" },
5205
+ React.createElement(
5206
+ "button",
5207
+ {
5208
+ type: "button",
5209
+ className: "collab-avatar collab-avatar-more",
5210
+ onMouseEnter: function (e) { cancelOverflowClose(); setCollabOverflowAnchor(anchorFrom(e.currentTarget)); },
5211
+ onMouseLeave: scheduleOverflowClose,
5212
+ onClick: function (e) {
5213
+ var el = e.currentTarget;
5214
+ setCollabOverflowAnchor(function (prev) { return prev ? null : anchorFrom(el); });
5215
+ }
5216
+ },
5217
+ "+" + collabOverflowIds.length
4776
5218
  )
4777
- );
4778
- })
5219
+ )
5220
+ )
4779
5221
  ),
4780
5222
  React.createElement("div", { className: "statusbar-sep" }),
4781
5223
  React.createElement(
@@ -4805,6 +5247,31 @@ var MbeditorApp = function MbeditorApp() {
4805
5247
  )
4806
5248
  )
4807
5249
  ),
5250
+ collabOverflowOpen && React.createElement(
5251
+ "div",
5252
+ {
5253
+ className: "collab-overflow-popover",
5254
+ style: { top: collabOverflowAnchor.top + 'px', right: collabOverflowAnchor.right + 'px' },
5255
+ onMouseEnter: cancelOverflowClose,
5256
+ onMouseLeave: scheduleOverflowClose
5257
+ },
5258
+ collabOverflowIds.map(function (cid) {
5259
+ var info = collabPeerInfo(cid);
5260
+ return React.createElement(
5261
+ "button",
5262
+ {
5263
+ key: cid,
5264
+ type: "button",
5265
+ className: "collab-overflow-row" + (info.following ? " is-following" : ""),
5266
+ onClick: function () { toggleFollow(cid); }
5267
+ },
5268
+ React.createElement("span", { className: "collab-overflow-swatch", style: { background: info.colour } }),
5269
+ React.createElement("span", { className: "collab-overflow-name" }, info.name),
5270
+ React.createElement("span", { className: "collab-overflow-file" }, info.file ? info.file.split('/').pop() : 'No file open'),
5271
+ info.following && React.createElement("i", { className: "fas fa-eye collab-overflow-eye" })
5272
+ );
5273
+ })
5274
+ ),
4808
5275
  collabHover && (function () {
4809
5276
  var isMe = collabHover.cid === '__me__';
4810
5277
  var peer = isMe ? null : collabRoster[collabHover.cid];
@@ -4893,6 +5360,15 @@ var MbeditorApp = function MbeditorApp() {
4893
5360
  );
4894
5361
  })(),
4895
5362
  showHelp && React.createElement(ShortcutHelp, { onClose: function () { return setShowHelp(false); } }),
5363
+ showSettings && React.createElement(window.SettingsModal || SettingsModal, {
5364
+ editorPrefs: editorPrefs,
5365
+ setEditorPrefs: setEditorPrefs,
5366
+ rubocopAvailable: rubocopAvailable,
5367
+ rubocopConfigPath: rubocopConfigPath,
5368
+ onOpenRubocopConfig: function () { handleSelectFile(rubocopConfigPath, rubocopConfigPath.split('/').pop()); },
5369
+ onReset: function () { setEditorPrefs(Object.assign({}, DEFAULT_EDITOR_PREFS)); },
5370
+ onClose: function () { return setShowSettings(false); }
5371
+ }),
4896
5372
  React.createElement(
4897
5373
  "div",
4898
5374
  { className: "ide-body", id: "ide-body-container" },
@@ -4937,7 +5413,7 @@ var MbeditorApp = function MbeditorApp() {
4937
5413
  "button",
4938
5414
  {
4939
5415
  type: "button",
4940
- className: "ide-activity-btn" + (activeTab && activeTab.isModelGraph ? ' active' : ''),
5416
+ className: "ide-activity-btn" + (showModelGraphView ? ' active' : ''),
4941
5417
  title: "Model graph",
4942
5418
  onClick: function() { handleActivityBarClick('models'); }
4943
5419
  },
@@ -4951,16 +5427,16 @@ var MbeditorApp = function MbeditorApp() {
4951
5427
  "button",
4952
5428
  {
4953
5429
  type: "button",
4954
- className: "ide-activity-btn" + (activeTab && activeTab.isSettings ? ' active' : ''),
5430
+ className: "ide-activity-btn" + (showSettings ? ' active' : ''),
4955
5431
  title: "Editor Preferences",
4956
- onClick: openSettingsTab
5432
+ onClick: function() { handleActivityBarClick('settings'); }
4957
5433
  },
4958
5434
  React.createElement("i", { className: "fas fa-cog" })
4959
5435
  )
4960
5436
  )
4961
5437
  ),
4962
- /* Panel content — shown when not collapsed and not in zen mode */
4963
- !sidebarCollapsed && !zenMode && React.createElement(
5438
+ /* Panel content — shown when not collapsed, not in zen mode, and not showing the model graph */
5439
+ !sidebarCollapsed && !zenMode && !showModelGraphView && React.createElement(
4964
5440
  "div",
4965
5441
  { className: "ide-sidebar", style: { width: sidebarWidth + "px" } },
4966
5442
  React.createElement("div", { className: "sidebar-panel-title" },
@@ -4998,108 +5474,126 @@ var MbeditorApp = function MbeditorApp() {
4998
5474
  React.createElement(
4999
5475
  "div",
5000
5476
  { style: { marginBottom: "12px" } },
5001
- state.panes.map(function (pane) {
5002
- if (pane.tabs.length === 0) return null;
5003
- var isPane2 = pane.id === 2;
5004
- return React.createElement(
5005
- "div",
5006
- {
5007
- key: pane.id,
5008
- className: "open-editors-group",
5009
- style: { marginBottom: pane.id === 1 && state.panes[1].tabs.length > 0 ? "10px" : "0" }
5010
- },
5011
- React.createElement(
5477
+ (function () {
5478
+ // VS Code drops the "GROUP N" sub-header when only one group
5479
+ // is populated — it's only useful to tell groups apart.
5480
+ var activePanes = state.panes.filter(function (p) { return p.tabs.length > 0; });
5481
+ var showGroupHeaders = activePanes.length > 1;
5482
+ return state.panes.map(function (pane) {
5483
+ if (pane.tabs.length === 0) return null;
5484
+ var isPane2 = pane.id === 2;
5485
+ return React.createElement(
5012
5486
  "div",
5013
- { className: "ide-sidebar-header open-editors-group-header" },
5014
- React.createElement(
5015
- "span",
5016
- { className: "open-editors-group-title" },
5017
- "GROUP ",
5018
- pane.id
5487
+ {
5488
+ key: pane.id,
5489
+ className: "open-editors-group",
5490
+ style: { marginBottom: pane.id === 1 && state.panes[1].tabs.length > 0 ? "10px" : "0" }
5491
+ },
5492
+ showGroupHeaders && React.createElement(
5493
+ "div",
5494
+ { className: "ide-sidebar-header open-editors-group-header" },
5495
+ React.createElement(
5496
+ "span",
5497
+ { className: "open-editors-group-title" },
5498
+ "GROUP ",
5499
+ pane.id
5500
+ ),
5501
+ React.createElement(
5502
+ SectionActionGroup,
5503
+ { ariaLabel: "Group " + pane.id + " actions", className: "collapsible-actions open-editors-group-actions" },
5504
+ React.createElement(SidebarActionButton, {
5505
+ title: "Close all editors in Group " + pane.id,
5506
+ ariaLabel: "Close all editors in Group " + pane.id,
5507
+ iconClass: "far fa-window-close",
5508
+ onClick: function (e) {
5509
+ e.stopPropagation();handleCloseEditorsInGroup(pane.id);
5510
+ }
5511
+ })
5512
+ )
5019
5513
  ),
5020
5514
  React.createElement(
5021
- SectionActionGroup,
5022
- { ariaLabel: "Group " + pane.id + " actions", className: "collapsible-actions open-editors-group-actions" },
5023
- React.createElement(SidebarActionButton, {
5024
- title: "Close all editors in Group " + pane.id,
5025
- ariaLabel: "Close all editors in Group " + pane.id,
5026
- iconClass: "far fa-window-close",
5027
- onClick: function (e) {
5028
- e.stopPropagation();handleCloseEditorsInGroup(pane.id);
5029
- }
5030
- })
5031
- )
5032
- ),
5033
- React.createElement(
5034
- "div",
5035
- { className: "file-tree" },
5036
- pane.tabs.map(function (tab) {
5037
- return React.createElement(
5038
- "div",
5039
- {
5040
- key: tab.id,
5041
- className: "tree-item " + (pane.activeTabId === tab.id && state.focusedPaneId === pane.id ? "active" : ""),
5042
- // The name ellipsises in a narrow sidebar, so the row
5043
- // carries the full path the way file-tree rows do.
5044
- title: tab.path || tab.name,
5045
- onClick: function () {
5046
- if (tab.path && !tab.path.startsWith('mbeditor://') && tab.path !== '__settings__') {
5047
- handleNodeSelect({ path: tab.path, name: tab.name, type: 'file' });
5048
- }
5049
- TabManager.focusPane(pane.id);TabManager.switchTab(pane.id, tab.id);
5050
- }
5051
- },
5052
- React.createElement("i", { className: "tree-item-icon " + (window.getFileIcon ? window.getFileIcon(tab.name) : 'far fa-file-code') + " tree-file-icon" }),
5053
- React.createElement(
5054
- "div",
5055
- // minWidth:0 on both the row's name cell and the label
5056
- // itself: without it a flex item refuses to shrink
5057
- // below its content, so a long filename pushed out
5058
- // under the (formerly absolute) action buttons.
5059
- { className: "tree-item-name", style: { display: 'flex', alignItems: 'center', minWidth: 0 } },
5060
- React.createElement(
5061
- "span",
5062
- { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', minWidth: 0 } },
5063
- tab.name
5064
- ),
5065
- tab.dirty && React.createElement("i", { className: "fas fa-circle", style: { fontSize: '5px', color: '#e3d286', marginLeft: '6px', marginTop: '1px', flexShrink: 0 } })
5066
- ),
5067
- React.createElement(
5515
+ "div",
5516
+ { className: "file-tree" },
5517
+ pane.tabs.map(function (tab) {
5518
+ var isActive = pane.activeTabId === tab.id && state.focusedPaneId === pane.id;
5519
+ var dir = tab.path ? tab.path.split('/').slice(0, -1).join('/') : '';
5520
+ return React.createElement(
5068
5521
  "div",
5069
- // In flow, not absolute — the buttons now claim their
5070
- // own width so the name truncates instead of running
5071
- // underneath them.
5072
- { className: "tab-actions", style: { display: 'flex', alignItems: 'center', flexShrink: 0, marginLeft: 'auto' } },
5522
+ {
5523
+ key: tab.id,
5524
+ className: "tree-item open-editors-item " + (isActive ? "active" : ""),
5525
+ // The name ellipsises in a narrow sidebar, so the row
5526
+ // carries the full path the way file-tree rows do.
5527
+ title: tab.path || tab.name,
5528
+ onClick: function () {
5529
+ if (tab.path && !tab.path.startsWith('mbeditor://')) {
5530
+ handleNodeSelect({ path: tab.path, name: tab.name, type: 'file' });
5531
+ }
5532
+ TabManager.focusPane(pane.id);TabManager.switchTab(pane.id, tab.id);
5533
+ }
5534
+ },
5535
+ React.createElement("i", { className: "tree-item-icon " + (window.getFileIcon ? window.getFileIcon(tab.name) : 'far fa-file-code') + " tree-file-icon" }),
5073
5536
  React.createElement(
5074
5537
  "div",
5075
- { className: "tab-split", onClick: function (e) {
5076
- e.stopPropagation();TabManager.moveTabToPane(pane.id, pane.id === 1 ? 2 : 1, tab.id);
5077
- }, style: { padding: '0 4px', cursor: 'pointer', opacity: 0.6 }, title: "Move to Group " + (pane.id === 1 ? 2 : 1) },
5078
- React.createElement("i", { className: isPane2 ? "fas fa-chevron-left" : "fas fa-chevron-right" })
5538
+ // minWidth:0 on both the row's name cell and the label
5539
+ // itself: without it a flex item refuses to shrink
5540
+ // below its content, so a long filename pushed out
5541
+ // under the (formerly absolute) action buttons.
5542
+ { className: "tree-item-name", style: { display: 'flex', alignItems: 'center', minWidth: 0 } },
5543
+ React.createElement(
5544
+ "span",
5545
+ { className: "open-editors-name", style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', minWidth: 0, flexShrink: 0 } },
5546
+ tab.name
5547
+ ),
5548
+ dir && React.createElement(
5549
+ "span",
5550
+ { className: "open-editors-dir" },
5551
+ dir
5552
+ )
5079
5553
  ),
5080
5554
  React.createElement(
5081
5555
  "div",
5082
- { className: "tab-close", onClick: function (e) {
5083
- e.stopPropagation();requestCloseTab(pane.id, tab.id);
5084
- }, style: { padding: '0 4px', cursor: 'pointer', opacity: 0.6 },
5085
- // See TabBar: role="button" would pick up Pico's
5086
- // button skin from the host app and square this off.
5087
- title: "Close " + tab.name + (tab.dirty ? " (unsaved changes)" : "") },
5088
- React.createElement("i", { className: "fas fa-times" })
5556
+ // In flow, not absolute — the buttons now claim their
5557
+ // own width so the name truncates instead of running
5558
+ // underneath them.
5559
+ { className: "tab-actions", style: { display: 'flex', alignItems: 'center', flexShrink: 0, marginLeft: 'auto' } },
5560
+ React.createElement(
5561
+ "div",
5562
+ { className: "tab-split", onClick: function (e) {
5563
+ e.stopPropagation();TabManager.moveTabToPane(pane.id, pane.id === 1 ? 2 : 1, tab.id);
5564
+ }, style: { padding: '0 4px', cursor: 'pointer' }, title: "Move to Group " + (pane.id === 1 ? 2 : 1) },
5565
+ React.createElement("i", { className: isPane2 ? "fas fa-chevron-left" : "fas fa-chevron-right" })
5566
+ ),
5567
+ React.createElement(
5568
+ "div",
5569
+ { className: "open-editors-close-slot" },
5570
+ tab.dirty && React.createElement("i", { className: "fas fa-circle open-editors-dirty-dot", 'aria-hidden': 'true' }),
5571
+ React.createElement(
5572
+ "button",
5573
+ { type: "button", className: "tab-close", onClick: function (e) {
5574
+ e.stopPropagation();requestCloseTab(pane.id, tab.id);
5575
+ }, style: { padding: '0 4px' },
5576
+ title: "Close " + tab.name + (tab.dirty ? " (unsaved changes)" : ""),
5577
+ 'aria-label': "Close " + tab.name + (tab.dirty ? " (unsaved changes)" : "") },
5578
+ React.createElement("i", { className: "fas fa-times" })
5579
+ )
5580
+ )
5089
5581
  )
5090
- )
5091
- );
5092
- })
5093
- )
5094
- );
5095
- })
5582
+ );
5583
+ })
5584
+ )
5585
+ );
5586
+ });
5587
+ })()
5096
5588
  )
5097
5589
  )
5098
5590
  ),
5099
- state.panes.flatMap(function (p) { return p.tabs; }).length > 0 && React.createElement(
5100
- "div",
5101
- { className: "open-editors-resize-handle", onMouseDown: startOpenEditorsResize }
5102
- ),
5591
+ state.panes.flatMap(function (p) { return p.tabs; }).length > 0 && React.createElement((window.Gutter || Gutter), {
5592
+ orientation: 'horizontal',
5593
+ label: "Resize open editors list",
5594
+ active: activeResizeMode === 'openeditors',
5595
+ onDragStart: startOpenEditorsResize
5596
+ }),
5103
5597
  React.createElement(
5104
5598
  "div",
5105
5599
  { className: "ide-sidebar-scrollable" },
@@ -5107,10 +5601,7 @@ var MbeditorApp = function MbeditorApp() {
5107
5601
  CollapsibleSection,
5108
5602
  {
5109
5603
  title: projectSectionTitle,
5110
- isCollapsed: collapsedSections.projects,
5111
- onToggle: function (isCollapsed) {
5112
- return handleToggleSection('projects', isCollapsed);
5113
- },
5604
+ collapsible: false,
5114
5605
  actions: React.createElement(
5115
5606
  SectionActionGroup,
5116
5607
  { ariaLabel: "Project actions" },
@@ -5145,10 +5636,15 @@ var MbeditorApp = function MbeditorApp() {
5145
5636
  disabled: !!loading.createDir
5146
5637
  }),
5147
5638
  React.createElement(SidebarActionButton, {
5148
- title: "Upload files",
5639
+ title: "Upload files to the workspace root",
5149
5640
  iconClass: 'fas fa-upload',
5641
+ // Defaults to the workspace root, not whatever happens to be
5642
+ // selected in the tree: an upload that silently follows the
5643
+ // selection lands files somewhere the user was not looking,
5644
+ // and the count reads as a success either way. The context
5645
+ // menu's "Upload Files Here..." is the way to target a folder.
5150
5646
  onClick: function () {
5151
- return openImportDialog(selectedTreeNode);
5647
+ return openImportDialog(null);
5152
5648
  }
5153
5649
  }),
5154
5650
  React.createElement(SidebarActionButton, {
@@ -5178,6 +5674,7 @@ var MbeditorApp = function MbeditorApp() {
5178
5674
  activePath: editorPrefs.autoRevealInExplorer !== false ? (activeTab && activeTab.path) : null,
5179
5675
  selectedPaths: selectedPaths,
5180
5676
  anchorPath: selectedTreePath,
5677
+ revealTarget: revealTarget,
5181
5678
  onNodeSelect: handleNodeSelect,
5182
5679
  onMultiSelect: handleMultiSelect,
5183
5680
  onMove: handleMoveNodes,
@@ -5516,18 +6013,32 @@ var MbeditorApp = function MbeditorApp() {
5516
6013
  })()
5517
6014
  )
5518
6015
  ),
5519
- /* Sidebar resize divider — only when panel is open */
5520
- !sidebarCollapsed && !zenMode && React.createElement("div", {
5521
- className: "panel-divider sidebar-divider " + (activeResizeMode === 'sidebar' ? 'active' : ''),
5522
- onMouseDown: startSidebarResize,
5523
- role: "separator",
5524
- "aria-orientation": "vertical",
5525
- "aria-label": "Resize explorer panel"
6016
+ // Collapsed: the same gutter is a handle. Drag it right and the
6017
+ // explorer snaps open; there is no intermediate width to preview.
6018
+ sidebarCollapsed && !zenMode && !showModelGraphView && React.createElement((window.Gutter || Gutter), {
6019
+ orientation: 'vertical',
6020
+ label: "Drag right to open the explorer",
6021
+ onSnap: function () { setSidebarCollapsed(false); }
5526
6022
  }),
5527
- // Column wrapping the split panes and the bottom drawers. ide-main is a
5528
- // row of panes, so the drawers need a vertical parent to push against;
5529
- // as absolute overlays they covered the editor instead.
5530
- React.createElement(
6023
+ /* Sidebar resize gutter — only when panel is open */
6024
+ !sidebarCollapsed && !zenMode && !showModelGraphView && React.createElement((window.Gutter || Gutter), {
6025
+ orientation: 'vertical',
6026
+ label: "Resize explorer panel",
6027
+ active: activeResizeMode === 'sidebar',
6028
+ onDragStart: startSidebarResize
6029
+ }),
6030
+ // The graph replaces the whole column — no tab bar, no panes, no drawers —
6031
+ // rather than living inside ide-main, so it gets the full center width.
6032
+ showModelGraphView ? React.createElement(
6033
+ "div",
6034
+ { className: "ide-model-graph-view" },
6035
+ React.createElement(ModelGraph, {
6036
+ graph: modelGraph,
6037
+ loading: modelGraphLoading,
6038
+ onRefresh: function () { loadModelGraph(true); },
6039
+ onOpenModel: function (model) { openSchemaModal(model.name); }
6040
+ })
6041
+ ) : React.createElement(
5531
6042
  "div",
5532
6043
  { className: "ide-center-column" },
5533
6044
  React.createElement(
@@ -5625,13 +6136,6 @@ var MbeditorApp = function MbeditorApp() {
5625
6136
  commits: pActiveTab.commits || [],
5626
6137
  onSelectCommit: handleSelectCommit
5627
6138
  });
5628
- } else if (pActiveTab.isModelGraph) {
5629
- content = React.createElement(ModelGraph, {
5630
- graph: modelGraph,
5631
- loading: modelGraphLoading,
5632
- onRefresh: function () { loadModelGraph(true); },
5633
- onOpenModel: function (model) { openSchemaModal(model.name); }
5634
- });
5635
6139
  } else if (pActiveTab.isChangelog) {
5636
6140
  content = React.createElement(ChangelogView, {
5637
6141
  changelogState: changelogState,
@@ -5644,597 +6148,12 @@ var MbeditorApp = function MbeditorApp() {
5644
6148
  }
5645
6149
  }
5646
6150
  });
5647
- } else if (pActiveTab.isSettings) {
5648
- content = React.createElement(
5649
- 'div',
5650
- { className: 'ide-settings-tab-content' },
5651
- React.createElement(
5652
- 'div',
5653
- { className: 'ide-settings-body' },
5654
-
5655
- /* ── Appearance ──────────────────────────────── */
5656
- React.createElement('div', { className: 'ide-settings-section-header' }, 'Appearance'),
5657
- React.createElement(
5658
- 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Color theme for the editor' },
5659
- React.createElement('span', { className: 'ide-settings-label' }, 'Theme'),
5660
- React.createElement(
5661
- 'select', {
5662
- value: editorPrefs.theme || 'vs-dark',
5663
- onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { theme: e.target.value }); }); }
5664
- },
5665
- React.createElement('option', { value: 'vs-dark' }, 'Dark'),
5666
- React.createElement('option', { value: 'vs' }, 'Light'),
5667
- React.createElement('option', { value: 'hc-black' }, 'HC Dark'),
5668
- React.createElement('option', { value: 'hc-light' }, 'HC Light'),
5669
- React.createElement('option', { value: 'dracula' }, 'Dracula'),
5670
- React.createElement('option', { value: 'night-owl' }, 'Night Owl'),
5671
- React.createElement('option', { value: 'monokai' }, 'Monokai'),
5672
- React.createElement('option', { value: 'nord' }, 'Nord'),
5673
- React.createElement('option', { value: 'github-dark' }, 'GitHub Dark'),
5674
- React.createElement('option', { value: 'tomorrow-night' }, 'Tomorrow Night'),
5675
- React.createElement('option', { value: 'github-light' }, 'GitHub Light')
5676
- )
5677
- ),
5678
- React.createElement(
5679
- 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Editor font size in pixels (8–32)' },
5680
- React.createElement('span', { className: 'ide-settings-label' }, 'Font size'),
5681
- React.createElement('input', {
5682
- key: String(editorPrefs.fontSize || 13),
5683
- type: 'number', min: '8', max: '32', step: '1',
5684
- className: 'ide-settings-input',
5685
- defaultValue: editorPrefs.fontSize || 13,
5686
- onChange: function(e) {
5687
- var v = parseInt(e.target.value, 10);
5688
- if (v >= 8 && v <= 32) setEditorPrefs(function(p) { return Object.assign({}, p, { fontSize: v }); });
5689
- },
5690
- onBlur: function(e) {
5691
- var v = parseInt(e.target.value, 10);
5692
- if (isNaN(v) || v < 8 || v > 32) e.target.value = String(editorPrefs.fontSize || 13);
5693
- }
5694
- })
5695
- ),
5696
- React.createElement(
5697
- 'label', { className: 'ide-settings-row-full', title: 'Font stack used in the editor — the first font available on your system is used' },
5698
- React.createElement('span', { className: 'ide-settings-label' }, 'Font family'),
5699
- React.createElement('input', {
5700
- type: 'text',
5701
- className: 'ide-settings-input ide-settings-input-wide',
5702
- value: editorPrefs.fontFamily || "'JetBrains Mono', 'Fira Code', Consolas, 'Courier New', monospace",
5703
- onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { fontFamily: e.target.value }); }); }
5704
- })
5705
- ),
5706
- React.createElement(
5707
- 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Row height in pixels. 0 = auto (roughly font size × 1.5)' },
5708
- React.createElement('span', { className: 'ide-settings-label' }, 'Line height (0=auto)'),
5709
- React.createElement('input', {
5710
- key: String(editorPrefs.lineHeight != null ? editorPrefs.lineHeight : 0),
5711
- type: 'number', min: '0', max: '100', step: '1',
5712
- className: 'ide-settings-input',
5713
- defaultValue: editorPrefs.lineHeight != null ? editorPrefs.lineHeight : 0,
5714
- onChange: function(e) {
5715
- var v = parseInt(e.target.value, 10);
5716
- if (!isNaN(v) && v >= 0 && v <= 100) setEditorPrefs(function(p) { return Object.assign({}, p, { lineHeight: v }); });
5717
- },
5718
- onBlur: function(e) {
5719
- var v = parseInt(e.target.value, 10);
5720
- if (isNaN(v) || v < 0 || v > 100) e.target.value = String(editorPrefs.lineHeight != null ? editorPrefs.lineHeight : 0);
5721
- }
5722
- })
5723
- ),
5724
- React.createElement(
5725
- 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Extra space between characters in pixels. 0 = default' },
5726
- React.createElement('span', { className: 'ide-settings-label' }, 'Letter spacing (px)'),
5727
- React.createElement('input', {
5728
- key: String(editorPrefs.letterSpacing != null ? editorPrefs.letterSpacing : 0),
5729
- type: 'number', min: '-5', max: '20', step: '0.5',
5730
- className: 'ide-settings-input',
5731
- defaultValue: editorPrefs.letterSpacing != null ? editorPrefs.letterSpacing : 0,
5732
- onChange: function(e) {
5733
- var v = parseFloat(e.target.value);
5734
- if (!isNaN(v) && v >= -5 && v <= 20) setEditorPrefs(function(p) { return Object.assign({}, p, { letterSpacing: v }); });
5735
- },
5736
- onBlur: function(e) {
5737
- var v = parseFloat(e.target.value);
5738
- if (isNaN(v) || v < -5 || v > 20) e.target.value = String(editorPrefs.letterSpacing != null ? editorPrefs.letterSpacing : 0);
5739
- }
5740
- })
5741
- ),
5742
-
5743
- /* ── Indentation (unified editor + Prettier) ── */
5744
- React.createElement('div', { className: 'ide-settings-section-header' }, 'Indentation'),
5745
- React.createElement(
5746
- 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Number of spaces per indentation level (also sets Prettier tab width)' },
5747
- React.createElement('span', { className: 'ide-settings-label' }, 'Tab size'),
5748
- React.createElement('input', {
5749
- key: String(editorPrefs.tabSize || 4),
5750
- type: 'number', min: '1', max: '8', step: '1',
5751
- className: 'ide-settings-input',
5752
- defaultValue: editorPrefs.tabSize || 4,
5753
- onChange: function(e) {
5754
- var v = parseInt(e.target.value, 10);
5755
- if (v >= 1 && v <= 8) setEditorPrefs(function(p) { return Object.assign({}, p, { tabSize: v }); });
5756
- },
5757
- onBlur: function(e) {
5758
- var v = parseInt(e.target.value, 10);
5759
- if (isNaN(v) || v < 1 || v > 8) e.target.value = String(editorPrefs.tabSize || 4);
5760
- }
5761
- })
5762
- ),
5763
- React.createElement(
5764
- 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Insert spaces instead of tab characters when pressing Tab' },
5765
- React.createElement('span', { className: 'ide-settings-label' }, 'Use spaces'),
5766
- React.createElement('input', {
5767
- type: 'checkbox',
5768
- className: 'ide-settings-checkbox',
5769
- checked: !!(editorPrefs.insertSpaces),
5770
- onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { insertSpaces: v }); }); }
5771
- })
5772
- ),
5773
-
5774
- /* ── Editor ──────────────────────────────────── */
5775
- React.createElement('div', { className: 'ide-settings-section-header' }, 'Editor'),
5776
- React.createElement(
5777
- 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'How long lines are handled — Off: scroll horizontally, On: wrap at viewport width, Column: wrap at a fixed column' },
5778
- React.createElement('span', { className: 'ide-settings-label' }, 'Word wrap'),
5779
- React.createElement(
5780
- 'select', {
5781
- value: editorPrefs.wordWrap || 'off',
5782
- onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { wordWrap: e.target.value }); }); }
5783
- },
5784
- React.createElement('option', { value: 'off' }, 'Off'),
5785
- React.createElement('option', { value: 'on' }, 'On'),
5786
- React.createElement('option', { value: 'wordWrapColumn' }, 'Column')
5787
- )
5788
- ),
5789
- React.createElement(
5790
- 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Show line numbers in the gutter — On, Off, or Relative (useful with Vim mode)' },
5791
- React.createElement('span', { className: 'ide-settings-label' }, 'Line numbers'),
5792
- React.createElement(
5793
- 'select', {
5794
- value: editorPrefs.lineNumbers || 'on',
5795
- onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { lineNumbers: e.target.value }); }); }
5796
- },
5797
- React.createElement('option', { value: 'on' }, 'On'),
5798
- React.createElement('option', { value: 'off' }, 'Off'),
5799
- React.createElement('option', { value: 'relative' }, 'Relative')
5800
- )
5801
- ),
5802
- React.createElement(
5803
- 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Render whitespace characters visually — None, Selection only, Boundary (leading/trailing), or All' },
5804
- React.createElement('span', { className: 'ide-settings-label' }, 'Whitespace'),
5805
- React.createElement(
5806
- 'select', {
5807
- value: editorPrefs.renderWhitespace || 'none',
5808
- onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { renderWhitespace: e.target.value }); }); }
5809
- },
5810
- React.createElement('option', { value: 'none' }, 'None'),
5811
- React.createElement('option', { value: 'selection' }, 'Selection'),
5812
- React.createElement('option', { value: 'boundary' }, 'Boundary'),
5813
- React.createElement('option', { value: 'all' }, 'All')
5814
- )
5815
- ),
5816
- React.createElement(
5817
- 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Show a scaled-down overview of the file on the right edge of the editor' },
5818
- React.createElement('span', { className: 'ide-settings-label' }, 'Minimap'),
5819
- React.createElement('input', {
5820
- type: 'checkbox',
5821
- className: 'ide-settings-checkbox',
5822
- checked: !!(editorPrefs.minimap),
5823
- onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { minimap: v }); }); }
5824
- })
5825
- ),
5826
- React.createElement(
5827
- 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Allow scrolling past the last line so it can be positioned at the top of the viewport' },
5828
- React.createElement('span', { className: 'ide-settings-label' }, 'Scroll past end'),
5829
- React.createElement('input', {
5830
- type: 'checkbox',
5831
- className: 'ide-settings-checkbox',
5832
- checked: !!(editorPrefs.scrollBeyondLastLine),
5833
- onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { scrollBeyondLastLine: v }); }); }
5834
- })
5835
- ),
5836
- React.createElement(
5837
- 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Colorize matching bracket pairs with distinct colors to make nesting easier to read' },
5838
- React.createElement('span', { className: 'ide-settings-label' }, 'Bracket colors'),
5839
- React.createElement('input', {
5840
- type: 'checkbox',
5841
- className: 'ide-settings-checkbox',
5842
- checked: !!(editorPrefs.bracketPairColorization),
5843
- onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { bracketPairColorization: v }); }); }
5844
- })
5845
- ),
5846
- React.createElement(
5847
- 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Enable Vim keybindings (Normal/Insert/Visual modes). Press Escape to return to Normal mode.' },
5848
- React.createElement('span', { className: 'ide-settings-label' }, 'Vim mode'),
5849
- React.createElement('input', {
5850
- type: 'checkbox',
5851
- className: 'ide-settings-checkbox',
5852
- checked: !!(editorPrefs.vimMode),
5853
- onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { vimMode: v }); }); }
5854
- })
5855
- ),
5856
- React.createElement(
5857
- 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'When to insert a matching closing bracket automatically' },
5858
- React.createElement('span', { className: 'ide-settings-label' }, 'Auto-close brackets'),
5859
- React.createElement(
5860
- 'select', {
5861
- value: editorPrefs.autoClosingBrackets || 'always',
5862
- onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { autoClosingBrackets: e.target.value }); }); }
5863
- },
5864
- React.createElement('option', { value: 'always' }, 'Always'),
5865
- React.createElement('option', { value: 'languageDefined' }, 'Per language rules'),
5866
- React.createElement('option', { value: 'beforeWhitespace' }, 'Only before whitespace'),
5867
- React.createElement('option', { value: 'never' }, 'Never')
5868
- )
5869
- ),
5870
- React.createElement(
5871
- 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'When to insert a matching closing quote automatically' },
5872
- React.createElement('span', { className: 'ide-settings-label' }, 'Auto-close quotes'),
5873
- React.createElement(
5874
- 'select', {
5875
- value: editorPrefs.autoClosingQuotes || 'always',
5876
- onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { autoClosingQuotes: e.target.value }); }); }
5877
- },
5878
- React.createElement('option', { value: 'always' }, 'Always'),
5879
- React.createElement('option', { value: 'languageDefined' }, 'Per language rules'),
5880
- React.createElement('option', { value: 'beforeWhitespace' }, 'Only before whitespace'),
5881
- React.createElement('option', { value: 'never' }, 'Never')
5882
- )
5883
- ),
5884
- React.createElement(
5885
- 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'What to highlight on the current editor line' },
5886
- React.createElement('span', { className: 'ide-settings-label' }, 'Line highlight'),
5887
- React.createElement(
5888
- 'select', {
5889
- value: editorPrefs.renderLineHighlight || 'none',
5890
- onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { renderLineHighlight: e.target.value }); }); }
5891
- },
5892
- React.createElement('option', { value: 'none' }, 'None'),
5893
- React.createElement('option', { value: 'gutter' }, 'Line number only'),
5894
- React.createElement('option', { value: 'line' }, 'Current line background'),
5895
- React.createElement('option', { value: 'all' }, 'Line number + background')
5896
- )
5897
- ),
5898
- React.createElement(
5899
- 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Shape of the text cursor in the editor' },
5900
- React.createElement('span', { className: 'ide-settings-label' }, 'Cursor style'),
5901
- React.createElement(
5902
- 'select', {
5903
- value: editorPrefs.cursorStyle || 'line',
5904
- onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { cursorStyle: e.target.value }); }); }
5905
- },
5906
- React.createElement('option', { value: 'line' }, 'Line (|)'),
5907
- React.createElement('option', { value: 'block' }, 'Block (filled)'),
5908
- React.createElement('option', { value: 'underline' }, 'Underline (_)'),
5909
- React.createElement('option', { value: 'line-thin' }, 'Line thin'),
5910
- React.createElement('option', { value: 'block-outline' }, 'Block outline'),
5911
- React.createElement('option', { value: 'underline-thin' }, 'Underline thin')
5912
- )
5913
- ),
5914
- React.createElement(
5915
- 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Cursor animation style — Blink (on/off), Smooth (fade), Phase (offset fade), Expand (grow), or Solid (no animation)' },
5916
- React.createElement('span', { className: 'ide-settings-label' }, 'Cursor blinking'),
5917
- React.createElement(
5918
- 'select', {
5919
- value: editorPrefs.cursorBlinking || 'blink',
5920
- onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { cursorBlinking: e.target.value }); }); }
5921
- },
5922
- React.createElement('option', { value: 'blink' }, 'Blink (on/off)'),
5923
- React.createElement('option', { value: 'smooth' }, 'Smooth (fade)'),
5924
- React.createElement('option', { value: 'phase' }, 'Phase (offset fade)'),
5925
- React.createElement('option', { value: 'expand' }, 'Expand (grow/shrink)'),
5926
- React.createElement('option', { value: 'solid' }, 'Solid (no blink)')
5927
- )
5928
- ),
5929
- React.createElement(
5930
- 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Show collapse arrows next to foldable regions (functions, classes, blocks)' },
5931
- React.createElement('span', { className: 'ide-settings-label' }, 'Code folding'),
5932
- React.createElement('input', {
5933
- type: 'checkbox',
5934
- className: 'ide-settings-checkbox',
5935
- checked: editorPrefs.folding !== false,
5936
- onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { folding: v }); }); }
5937
- })
5938
- ),
5939
- React.createElement(
5940
- 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Animate scrolling instead of jumping instantly' },
5941
- React.createElement('span', { className: 'ide-settings-label' }, 'Smooth scrolling'),
5942
- React.createElement('input', {
5943
- type: 'checkbox',
5944
- className: 'ide-settings-checkbox',
5945
- checked: !!(editorPrefs.smoothScrolling),
5946
- onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { smoothScrolling: v }); }); }
5947
- })
5948
- ),
5949
- React.createElement(
5950
- 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Hold Ctrl (or Cmd) and scroll the mouse wheel to zoom the font size' },
5951
- React.createElement('span', { className: 'ide-settings-label' }, 'Ctrl+scroll to zoom'),
5952
- React.createElement('input', {
5953
- type: 'checkbox',
5954
- className: 'ide-settings-checkbox',
5955
- checked: !!(editorPrefs.mouseWheelZoom),
5956
- onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { mouseWheelZoom: v }); }); }
5957
- })
5958
- ),
5959
-
5960
- /* ── Behaviour ───────────────────────────────── */
5961
- React.createElement('div', { className: 'ide-settings-section-header' }, 'Behaviour'),
5962
- React.createElement(
5963
- 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'How aggressively the editor re-indents lines as you type' },
5964
- React.createElement('span', { className: 'ide-settings-label' }, 'Auto indent'),
5965
- React.createElement(
5966
- 'select', {
5967
- value: editorPrefs.autoIndent || 'full',
5968
- onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { autoIndent: e.target.value }); }); }
5969
- },
5970
- React.createElement('option', { value: 'none' }, 'None (disabled)'),
5971
- React.createElement('option', { value: 'keep' }, 'Keep current level'),
5972
- React.createElement('option', { value: 'brackets' }, 'Indent on { and ['),
5973
- React.createElement('option', { value: 'advanced' }, 'Language indent rules'),
5974
- React.createElement('option', { value: 'full' }, 'Full (language grammar)')
5975
- )
5976
- ),
5977
- React.createElement(
5978
- 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Whether pressing Enter accepts the highlighted autocomplete suggestion' },
5979
- React.createElement('span', { className: 'ide-settings-label' }, 'Accept suggestion on Enter'),
5980
- React.createElement(
5981
- 'select', {
5982
- value: editorPrefs.acceptSuggestionOnEnter || 'on',
5983
- onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { acceptSuggestionOnEnter: e.target.value }); }); }
5984
- },
5985
- React.createElement('option', { value: 'on' }, 'Always'),
5986
- React.createElement('option', { value: 'smart' }, 'Only when navigated (↑↓)'),
5987
- React.createElement('option', { value: 'off' }, 'Never (Tab only)')
5988
- )
5989
- ),
5990
- React.createElement(
5991
- 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Suggest completions based on words already present in open files' },
5992
- React.createElement('span', { className: 'ide-settings-label' }, 'Word-based suggestions'),
5993
- React.createElement(
5994
- 'select', {
5995
- value: editorPrefs.wordBasedSuggestions || 'matchingDocuments',
5996
- onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { wordBasedSuggestions: e.target.value }); }); }
5997
- },
5998
- React.createElement('option', { value: 'off' }, 'Off'),
5999
- React.createElement('option', { value: 'currentDocument' }, 'Current file only'),
6000
- React.createElement('option', { value: 'matchingDocuments' }, 'Same language files'),
6001
- React.createElement('option', { value: 'allDocuments' }, 'All open files')
6002
- )
6003
- ),
6004
- React.createElement(
6005
- '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 {})' },
6006
- React.createElement('span', { className: 'ide-settings-label' }, 'Format on type'),
6007
- React.createElement('input', {
6008
- type: 'checkbox',
6009
- className: 'ide-settings-checkbox',
6010
- checked: editorPrefs.formatOnType === true,
6011
- onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { formatOnType: v }); }); }
6012
- })
6013
- ),
6014
- React.createElement(
6015
- 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Format the file before every save — RuboCop -A for Ruby, Prettier for JS/JSX/CSS/HTML/Markdown' },
6016
- React.createElement('span', { className: 'ide-settings-label' }, 'Format on save'),
6017
- React.createElement('input', {
6018
- type: 'checkbox',
6019
- className: 'ide-settings-checkbox',
6020
- checked: editorPrefs.formatOnSave === true,
6021
- onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { formatOnSave: v }); }); }
6022
- })
6023
- ),
6024
- React.createElement(
6025
- 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Show autocomplete suggestions while typing (not just on trigger characters like .)' },
6026
- React.createElement('span', { className: 'ide-settings-label' }, 'Quick suggestions'),
6027
- React.createElement('input', {
6028
- type: 'checkbox',
6029
- className: 'ide-settings-checkbox',
6030
- checked: editorPrefs.quickSuggestions !== false,
6031
- onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { quickSuggestions: v }); }); }
6032
- })
6033
- ),
6034
-
6035
- /* ── Formatting (Prettier) ───────────────────── */
6036
- React.createElement('div', { className: 'ide-settings-section-header' }, 'Formatting'),
6037
- React.createElement(
6038
- 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Prettier: maximum line length before wrapping (40–200)' },
6039
- React.createElement('span', { className: 'ide-settings-label' }, 'Print width'),
6040
- React.createElement('input', {
6041
- key: String(editorPrefs.prettierPrintWidth != null ? editorPrefs.prettierPrintWidth : 80),
6042
- type: 'number', min: '40', max: '200', step: '1',
6043
- className: 'ide-settings-input',
6044
- defaultValue: editorPrefs.prettierPrintWidth != null ? editorPrefs.prettierPrintWidth : 80,
6045
- onChange: function(e) {
6046
- var v = parseInt(e.target.value, 10);
6047
- if (v >= 40 && v <= 200) setEditorPrefs(function(p) { return Object.assign({}, p, { prettierPrintWidth: v }); });
6048
- },
6049
- onBlur: function(e) {
6050
- var v = parseInt(e.target.value, 10);
6051
- if (isNaN(v) || v < 40 || v > 200) e.target.value = String(editorPrefs.prettierPrintWidth != null ? editorPrefs.prettierPrintWidth : 80);
6052
- }
6053
- })
6054
- ),
6055
- React.createElement(
6056
- 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Prettier: add trailing commas in multi-line expressions — All (ES2017+), ES5 (objects/arrays only), or None' },
6057
- React.createElement('span', { className: 'ide-settings-label' }, 'Trailing commas'),
6058
- React.createElement(
6059
- 'select', {
6060
- value: editorPrefs.prettierTrailingComma || 'all',
6061
- onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { prettierTrailingComma: e.target.value }); }); }
6062
- },
6063
- React.createElement('option', { value: 'all' }, 'All'),
6064
- React.createElement('option', { value: 'es5' }, 'ES5'),
6065
- React.createElement('option', { value: 'none' }, 'None')
6066
- )
6067
- ),
6068
- React.createElement(
6069
- 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Prettier: add semicolons at the end of statements' },
6070
- React.createElement('span', { className: 'ide-settings-label' }, 'Semicolons'),
6071
- React.createElement('input', {
6072
- type: 'checkbox',
6073
- className: 'ide-settings-checkbox',
6074
- checked: editorPrefs.prettierSemi !== false,
6075
- onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { prettierSemi: v }); }); }
6076
- })
6077
- ),
6078
- React.createElement(
6079
- 'label', { className: 'ide-settings-row ide-settings-row-check', title: "Prettier: use single quotes instead of double quotes for strings" },
6080
- React.createElement('span', { className: 'ide-settings-label' }, 'Single quotes'),
6081
- React.createElement('input', {
6082
- type: 'checkbox',
6083
- className: 'ide-settings-checkbox',
6084
- checked: !!editorPrefs.prettierSingleQuote,
6085
- onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { prettierSingleQuote: v }); }); }
6086
- })
6087
- ),
6088
- React.createElement(
6089
- 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Prettier: add spaces inside object literal braces, e.g. { a: 1 } vs {a: 1}' },
6090
- React.createElement('span', { className: 'ide-settings-label' }, 'Bracket spacing'),
6091
- React.createElement('input', {
6092
- type: 'checkbox',
6093
- className: 'ide-settings-checkbox',
6094
- checked: editorPrefs.prettierBracketSpacing !== false,
6095
- onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { prettierBracketSpacing: v }); }); }
6096
- })
6097
- ),
6098
-
6099
- /* ── Interface ───────────────────────────────── */
6100
- React.createElement('div', { className: 'ide-settings-section-header' }, 'Interface'),
6101
- React.createElement(
6102
- 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Automatically scroll the file explorer to reveal and highlight the file you are editing' },
6103
- React.createElement('span', { className: 'ide-settings-label' }, 'Explorer follows active file'),
6104
- React.createElement('input', {
6105
- type: 'checkbox',
6106
- className: 'ide-settings-checkbox',
6107
- checked: !!(editorPrefs.autoRevealInExplorer),
6108
- onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { autoRevealInExplorer: v }); }); }
6109
- })
6110
- ),
6111
- React.createElement(
6112
- 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Jump to a file in the explorer by typing its name when the sidebar is focused' },
6113
- React.createElement('span', { className: 'ide-settings-label' }, 'Explorer type-ahead'),
6114
- React.createElement('input', {
6115
- type: 'checkbox',
6116
- className: 'ide-settings-checkbox',
6117
- checked: !!(editorPrefs.fileTreeTypeahead !== false),
6118
- onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { fileTreeTypeahead: v }); }); }
6119
- })
6120
- ),
6121
- React.createElement(
6122
- 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Show hidden files and directories (those starting with a dot, e.g. .env, .gitignore) in the file explorer' },
6123
- React.createElement('span', { className: 'ide-settings-label' }, 'Show dotfiles'),
6124
- React.createElement('input', {
6125
- type: 'checkbox',
6126
- className: 'ide-settings-checkbox',
6127
- checked: !!(editorPrefs.showDotFiles),
6128
- onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { showDotFiles: v }); }); }
6129
- })
6130
- ),
6131
- React.createElement(
6132
- 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Scroll: tabs overflow horizontally with a scrollbar; Wrap: tabs flow onto multiple rows' },
6133
- React.createElement('span', { className: 'ide-settings-label' }, 'Tab bar layout'),
6134
- React.createElement(
6135
- 'select', {
6136
- value: editorPrefs.tabDisplayMode || 'scroll',
6137
- onChange: function(e) { var v = e.target.value; setEditorPrefs(function(p) { return Object.assign({}, p, { tabDisplayMode: v }); }); }
6138
- },
6139
- React.createElement('option', { value: 'scroll' }, 'Scroll'),
6140
- React.createElement('option', { value: 'wrap' }, 'Wrap (multi-row)')
6141
- )
6142
- ),
6143
- React.createElement(
6144
- 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Include folder names in the Quick Open picker (Ctrl+P / Cmd+P) results, not just files' },
6145
- React.createElement('span', { className: 'ide-settings-label' }, 'Quick Open: show folders'),
6146
- React.createElement('input', {
6147
- type: 'checkbox',
6148
- className: 'ide-settings-checkbox',
6149
- checked: !!(editorPrefs.quickOpenShowFolders),
6150
- onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { quickOpenShowFolders: v }); }); }
6151
- })
6152
- ),
6153
- React.createElement(
6154
- 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Hide toolbar button labels and show only icons, giving more horizontal space' },
6155
- React.createElement('span', { className: 'ide-settings-label' }, 'Toolbar: icons only'),
6156
- React.createElement('input', {
6157
- type: 'checkbox',
6158
- className: 'ide-settings-checkbox',
6159
- // The stored preference, not the derived value: at a
6160
- // narrow width the box would otherwise show as checked
6161
- // and unchecking it would appear to do nothing.
6162
- checked: !!(editorPrefs.toolbarIconOnly),
6163
- onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { toolbarIconOnly: v }); }); }
6164
- })
6165
- ),
6166
-
6167
- React.createElement(
6168
- 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Keep the search/replace text when switching between files in the editor' },
6169
- React.createElement('span', { className: 'ide-settings-label' }, 'Persist find state across files'),
6170
- React.createElement('input', {
6171
- type: 'checkbox',
6172
- className: 'ide-settings-checkbox',
6173
- checked: editorPrefs.persistFindState !== false,
6174
- onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { persistFindState: v }); }); }
6175
- })
6176
- ),
6177
- React.createElement(
6178
- 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Save which files are open per branch and restore them when switching branches. Disable to always start with a clean slate when switching.' },
6179
- React.createElement('span', { className: 'ide-settings-label' }, 'Restore tabs on branch switch'),
6180
- React.createElement('input', {
6181
- type: 'checkbox',
6182
- className: 'ide-settings-checkbox',
6183
- checked: editorPrefs.branchStateRestore !== false,
6184
- onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { branchStateRestore: v }); }); }
6185
- })
6186
- ),
6187
-
6188
- React.createElement(
6189
- 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Show the verb and path that route to each controller action after its def line, and mark public actions nothing routes to' },
6190
- React.createElement('span', { className: 'ide-settings-label' }, 'Controller route hints'),
6191
- React.createElement('input', {
6192
- type: 'checkbox',
6193
- className: 'ide-settings-checkbox',
6194
- checked: editorPrefs.routeHints !== false,
6195
- onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { routeHints: v }); }); }
6196
- })
6197
- ),
6198
-
6199
- /* ── RuboCop ─────────────────────────────────── */
6200
- React.createElement('div', { className: 'ide-settings-section-header' }, 'RuboCop'),
6201
- React.createElement(
6202
- 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Run RuboCop in the background and show lint warnings/errors as markers in the editor gutter' },
6203
- React.createElement('span', { className: 'ide-settings-label' }, 'Enable RuboCop linting'),
6204
- React.createElement('input', {
6205
- type: 'checkbox',
6206
- className: 'ide-settings-checkbox',
6207
- checked: editorPrefs.rubocopLintEnabled !== false,
6208
- onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { rubocopLintEnabled: v }); }); }
6209
- })
6210
- ),
6211
- rubocopAvailable && rubocopConfigPath ? React.createElement(
6212
- 'div', { className: 'ide-settings-row ide-settings-row-link' },
6213
- React.createElement('span', { className: 'ide-settings-label' }, 'Config file'),
6214
- React.createElement(
6215
- 'button', {
6216
- type: 'button',
6217
- className: 'ide-settings-config-link',
6218
- title: 'Open ' + rubocopConfigPath,
6219
- onClick: function() { handleSelectFile(rubocopConfigPath, rubocopConfigPath.split('/').pop()); }
6220
- },
6221
- React.createElement('i', { className: 'fas fa-file-alt', style: { marginRight: 5 } }),
6222
- rubocopConfigPath
6223
- )
6224
- ) : null,
6225
- React.createElement(
6226
- 'button',
6227
- {
6228
- className: 'ide-settings-reset-btn',
6229
- type: 'button',
6230
- title: 'Restore every editor preference on this page to its default',
6231
- onClick: function() { setEditorPrefs(Object.assign({}, DEFAULT_EDITOR_PREFS)); }
6232
- },
6233
- React.createElement('i', { className: 'fas fa-undo', style: { marginRight: 6 } }),
6234
- 'Reset to defaults'
6235
- )
6236
- )
6237
- );
6151
+ } else if (pActiveTab.isCombinedView) {
6152
+ content = React.createElement(CombinedFileView, {
6153
+ key: pActiveTab.id,
6154
+ paths: pActiveTab.combinedPaths || [],
6155
+ total: pActiveTab.combinedTotal || 0
6156
+ });
6238
6157
  } else if (pActiveTab.isDiff) {
6239
6158
  var _t = editorPrefs.theme || 'vs-dark';
6240
6159
  var isDiffDark = _t !== 'vs' && _t !== 'hc-light' && _t !== 'github-light';
@@ -6283,9 +6202,12 @@ var MbeditorApp = function MbeditorApp() {
6283
6202
  return React.createElement(
6284
6203
  React.Fragment,
6285
6204
  { key: pane.id },
6286
- idx === 1 && isSplit && React.createElement("div", {
6287
- className: "panel-divider pane-divider " + (activeResizeMode === 'pane' ? 'active' : ''),
6288
- onMouseDown: startPaneResize
6205
+ idx === 1 && isSplit && React.createElement((window.Gutter || Gutter), {
6206
+ orientation: 'vertical',
6207
+ className: 'ide-gutter-pane',
6208
+ label: "Resize editor groups",
6209
+ active: activeResizeMode === 'pane',
6210
+ onDragStart: startPaneResize
6289
6211
  }),
6290
6212
  React.createElement(
6291
6213
  "div",
@@ -6293,10 +6215,6 @@ var MbeditorApp = function MbeditorApp() {
6293
6215
  className: "ide-pane " + (isFocused ? 'focused' : '') + " " + (isDropTarget ? 'drop-target' : ''),
6294
6216
  style: { flexBasis: flexBasis, flexShrink: 0, flexGrow: 0, display: 'flex', flexDirection: 'column', minWidth: 0 },
6295
6217
  onClickCapture: function (e) {
6296
- // Do not steal click events from controls inside the Settings tab.
6297
- // Focusing the pane in capture phase can rerender before checkbox
6298
- // change events are processed, making toggles appear stuck.
6299
- if (e.target && e.target.closest && e.target.closest('.ide-settings-tab-content')) return;
6300
6218
  return TabManager.focusPane(pane.id);
6301
6219
  },
6302
6220
  onDragOver: function (e) {
@@ -6438,10 +6356,26 @@ var MbeditorApp = function MbeditorApp() {
6438
6356
  );
6439
6357
  })
6440
6358
  ),
6359
+ showLogPanel && !zenMode && React.createElement((window.Gutter || Gutter), {
6360
+ orientation: 'horizontal',
6361
+ label: "Resize log drawer",
6362
+ active: activeResizeMode === 'log',
6363
+ onDragStart: startDrawerResize('log', logHeight, 'mbeditorLogHeight')
6364
+ }),
6441
6365
  showLogPanel && !zenMode && React.createElement(window.LogPanel || LogPanel, {
6366
+ height: logHeight,
6442
6367
  onClose: function () { setShowLogPanel(false); }
6443
6368
  }),
6369
+ // With the drawer closed, the gutter above the status bar is a handle:
6370
+ // drag it up to open Problems at the dragged height.
6371
+ showProblemsPanel && !zenMode && React.createElement((window.Gutter || Gutter), {
6372
+ orientation: 'horizontal',
6373
+ label: "Resize problems drawer",
6374
+ active: activeResizeMode === 'problems',
6375
+ onDragStart: startDrawerResize('problems', problemsHeight, 'mbeditorProblemsHeight')
6376
+ }),
6444
6377
  showProblemsPanel && !zenMode && React.createElement(window.ProblemsPanel || ProblemsPanel, {
6378
+ height: problemsHeight,
6445
6379
  onClose: function () { setShowProblemsPanel(false); },
6446
6380
  onOpenFile: function (path, line, col) {
6447
6381
  handleSelectFile(path, path.split('/').pop(), line, col);
@@ -6456,14 +6390,20 @@ var MbeditorApp = function MbeditorApp() {
6456
6390
  ),
6457
6391
 
6458
6392
  // Right-side Git panel (children of ide-body, alongside sidebar and ide-main)
6459
- showGitPanel && !zenMode && React.createElement("div", {
6460
- className: "panel-divider gitpanel-divider " + (activeResizeMode === 'gitpanel' ? 'active' : ''),
6461
- onMouseDown: startGitPanelResize,
6462
- role: "separator",
6463
- "aria-orientation": "vertical",
6464
- "aria-label": "Resize git panel"
6393
+ !showGitPanel && !zenMode && !showModelGraphView && React.createElement((window.Gutter || Gutter), {
6394
+ orientation: 'vertical',
6395
+ label: "Drag left to open the git panel",
6396
+ snapDirection: -1,
6397
+ onSnap: function () { setShowGitPanel(true); }
6465
6398
  }),
6466
- showGitPanel && !zenMode && React.createElement(
6399
+ showGitPanel && !zenMode && !showModelGraphView && React.createElement((window.Gutter || Gutter), {
6400
+ orientation: 'vertical',
6401
+ className: 'ide-gutter-gitpanel',
6402
+ label: "Resize git panel",
6403
+ active: activeResizeMode === 'gitpanel',
6404
+ onDragStart: startGitPanelResize
6405
+ }),
6406
+ showGitPanel && !zenMode && !showModelGraphView && React.createElement(
6467
6407
  "div",
6468
6408
  { className: "ide-git-right-panel", style: { width: gitPanelWidth + "px" } },
6469
6409
  React.createElement(window.GitPanel || GitPanel, {
@@ -6479,6 +6419,19 @@ var MbeditorApp = function MbeditorApp() {
6479
6419
  })
6480
6420
  ),
6481
6421
  ),
6422
+ // ponytail: opens on release, no live preview; track the drag if that grates.
6423
+ !showProblemsPanel && !zenMode && !showModelGraphView && React.createElement((window.Gutter || Gutter), {
6424
+ orientation: 'horizontal',
6425
+ className: 'ide-gutter-drawer-open',
6426
+ label: "Drag up to open Problems",
6427
+ snapThreshold: 40,
6428
+ onSnap: function (dragged) {
6429
+ var h = Math.max(DRAWER_MIN_HEIGHT, dragged);
6430
+ try { window.localStorage.setItem('mbeditorProblemsHeight', String(h)); } catch (err) {}
6431
+ setProblemsHeight(h);
6432
+ setShowProblemsPanel(true);
6433
+ }
6434
+ }),
6482
6435
  React.createElement(
6483
6436
  "div",
6484
6437
  { className: "ide-statusbar" },
@@ -6515,6 +6468,15 @@ var MbeditorApp = function MbeditorApp() {
6515
6468
  React.createElement("i", { className: "fas fa-exclamation-triangle statusbar-problems-warning-icon" }),
6516
6469
  React.createElement("span", { className: "statusbar-problems-count" }, problemCounts.warnings)
6517
6470
  ),
6471
+ migrationsPending && React.createElement(
6472
+ "div",
6473
+ {
6474
+ className: "statusbar-migration",
6475
+ title: "Pending migrations \u2014 run `rails db:migrate`. Editing still works in the meantime."
6476
+ },
6477
+ React.createElement("i", { className: "fas fa-database" }),
6478
+ " Migrations pending"
6479
+ ),
6518
6480
  !serverOnline && (function () {
6519
6481
  var dirtyCount = state.panes.reduce(function (acc, p) {
6520
6482
  return acc + p.tabs.filter(function (t) { return t.dirty; }).length;
@@ -6574,6 +6536,7 @@ var MbeditorApp = function MbeditorApp() {
6574
6536
  },
6575
6537
  React.createElement("i", { className: "fas fa-paragraph" })
6576
6538
  ),
6539
+ React.createElement(CursorPosition, null),
6577
6540
  activeEOL && React.createElement(
6578
6541
  "button",
6579
6542
  {
@@ -6786,6 +6749,7 @@ var MbeditorApp = function MbeditorApp() {
6786
6749
  "div",
6787
6750
  {
6788
6751
  className: "context-menu",
6752
+ ref: contextMenuRef,
6789
6753
  style: { left: contextMenu.x, top: contextMenu.y },
6790
6754
  onClick: function (e) {
6791
6755
  return e.stopPropagation();
@@ -6830,16 +6794,24 @@ var MbeditorApp = function MbeditorApp() {
6830
6794
  return handleContextMenuAction('delete');
6831
6795
  } },
6832
6796
  React.createElement("i", { className: "far fa-trash-alt context-menu-icon" }),
6833
- " Delete"
6797
+ " " + deleteMenuLabel(contextMenu.node)
6834
6798
  ),
6835
6799
  React.createElement("div", { className: "context-menu-divider" }),
6836
- contextMenu.node && contextMenu.node.type === 'file' && React.createElement(
6800
+ contextMenu.node && (actsOnSelection(contextMenu.node) || contextMenu.node.type === 'folder') && React.createElement(
6801
+ "div",
6802
+ { className: "context-menu-item", onClick: function () {
6803
+ return handleContextMenuAction('combinedView');
6804
+ } },
6805
+ React.createElement("i", { className: "fas fa-layer-group context-menu-icon" }),
6806
+ " Open in Combined View"
6807
+ ),
6808
+ contextMenu.node && React.createElement(
6837
6809
  "div",
6838
6810
  { className: "context-menu-item", onClick: function () {
6839
6811
  return handleContextMenuAction('download');
6840
6812
  } },
6841
6813
  React.createElement("i", { className: "fas fa-download context-menu-icon" }),
6842
- " Download"
6814
+ " " + downloadMenuLabel(contextMenu.node)
6843
6815
  ),
6844
6816
  React.createElement(
6845
6817
  "div",
@@ -6937,9 +6909,20 @@ var MbeditorApp = function MbeditorApp() {
6937
6909
  importConflict && React.createElement(ImportConflictModal, {
6938
6910
  conflicts: importConflict.result.conflicts,
6939
6911
  errors: importConflict.result.errors,
6912
+ entries: importConflict.entries,
6913
+ targetFolderPath: importConflict.targetFolderPath,
6914
+ isDark: !['vs', 'hc-light', 'github-light'].includes(editorPrefs.theme || 'vs-dark'),
6915
+ editorPrefs: editorPrefs,
6940
6916
  onResolve: resolveImportConflict
6941
6917
  }),
6942
6918
 
6919
+ /* ── Paste & Place ─────────────────────────────────────────────────── */
6920
+ state.isPastePlaceVisible && React.createElement(PastePlaceModal, {
6921
+ isDark: !['vs', 'hc-light', 'github-light'].includes(editorPrefs.theme || 'vs-dark'),
6922
+ editorPrefs: editorPrefs,
6923
+ onClose: function () { EditorStore.setState({ isPastePlaceVisible: false }); }
6924
+ }),
6925
+
6943
6926
  /* ── Schema modal ──────────────────────────────────────────────────── */
6944
6927
  schemaModal && React.createElement(
6945
6928
  'div',
@@ -7033,4 +7016,6 @@ var MbeditorApp = function MbeditorApp() {
7033
7016
  };
7034
7017
 
7035
7018
  window.MbeditorApp = MbeditorApp;
7019
+ // Test-only: lets the system test seed/inspect the test-result cache directly.
7020
+ window.__mbeditorTestCache = { save: saveCachedTestResult, prefix: TEST_CACHE_PREFIX };
7036
7021
  /* TITLE BAR */ /* SIDEBAR */ /* EDITOR AREA */ /* STATUS BAR */ /* Right-click context menu */