mbeditor 0.5.3 → 0.7.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -0
  3. data/README.md +7 -0
  4. data/app/assets/javascripts/mbeditor/application.js +3 -0
  5. data/app/assets/javascripts/mbeditor/components/ChangelogView.js +145 -0
  6. data/app/assets/javascripts/mbeditor/components/DiffViewer.js +1 -1
  7. data/app/assets/javascripts/mbeditor/components/EditorPanel.js +359 -31
  8. data/app/assets/javascripts/mbeditor/components/FileTree.js +177 -116
  9. data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +952 -143
  10. data/app/assets/javascripts/mbeditor/components/TabBar.js +9 -0
  11. data/app/assets/javascripts/mbeditor/conflict_parser.js +48 -0
  12. data/app/assets/javascripts/mbeditor/editor_plugins.js +420 -67
  13. data/app/assets/javascripts/mbeditor/editor_store.js +1 -0
  14. data/app/assets/javascripts/mbeditor/file_service.js +34 -6
  15. data/app/assets/javascripts/mbeditor/git_service.js +2 -1
  16. data/app/assets/javascripts/mbeditor/history_service.js +177 -0
  17. data/app/assets/javascripts/mbeditor/search_service.js +1 -0
  18. data/app/assets/javascripts/mbeditor/tab_manager.js +8 -5
  19. data/app/assets/stylesheets/mbeditor/application.css +112 -0
  20. data/app/assets/stylesheets/mbeditor/editor.css +443 -78
  21. data/app/channels/mbeditor/editor_channel.rb +5 -41
  22. data/app/controllers/mbeditor/application_controller.rb +8 -1
  23. data/app/controllers/mbeditor/editors_controller.rb +276 -654
  24. data/app/controllers/mbeditor/git_controller.rb +2 -61
  25. data/app/services/mbeditor/availability_probe.rb +83 -0
  26. data/app/services/mbeditor/code_search_service.rb +42 -0
  27. data/app/services/mbeditor/editor_state_service.rb +91 -0
  28. data/app/services/mbeditor/exclusion_matcher.rb +23 -0
  29. data/app/services/mbeditor/file_operation_service.rb +68 -0
  30. data/app/services/mbeditor/file_tree_service.rb +69 -0
  31. data/app/services/mbeditor/git_combined_diff_service.rb +43 -0
  32. data/app/services/mbeditor/git_commit_detail_service.rb +46 -0
  33. data/app/services/mbeditor/git_info_service.rb +151 -0
  34. data/app/services/mbeditor/git_service.rb +36 -26
  35. data/app/services/mbeditor/js_definition_service.rb +59 -0
  36. data/app/services/mbeditor/js_members_service.rb +62 -0
  37. data/app/services/mbeditor/process_runner.rb +48 -0
  38. data/app/services/mbeditor/rails_related_files_service.rb +282 -0
  39. data/app/services/mbeditor/ruby_definition_service.rb +77 -101
  40. data/app/services/mbeditor/schema_service.rb +270 -0
  41. data/app/services/mbeditor/search_replace_service.rb +184 -0
  42. data/app/services/mbeditor/test_runner_service.rb +5 -27
  43. data/app/views/layouts/mbeditor/application.html.erb +2 -2
  44. data/config/routes.rb +8 -1
  45. data/lib/mbeditor/configuration.rb +4 -2
  46. data/lib/mbeditor/version.rb +1 -1
  47. data/public/monaco-editor/vs/language/css/cssMode.js +13 -0
  48. data/public/monaco-editor/vs/language/css/cssWorker.js +77 -0
  49. data/public/monaco-editor/vs/language/html/htmlMode.js +13 -0
  50. data/public/monaco-editor/vs/language/html/htmlWorker.js +454 -0
  51. data/public/monaco-editor/vs/language/json/jsonMode.js +19 -0
  52. data/public/monaco-editor/vs/language/json/jsonWorker.js +42 -0
  53. metadata +26 -3
  54. data/app/services/mbeditor/unused_methods_service.rb +0 -139
@@ -36,9 +36,17 @@ var EditorPanel = function EditorPanel(_ref) {
36
36
  var monacoRef = useRef(null);
37
37
  var latestContentRef = useRef('');
38
38
  var lastAppliedExternalVersionRef = useRef(0);
39
+ var conflictDecorationsRef = React.useRef([]);
40
+ var conflictBlocksRef = React.useRef([]);
39
41
  var aviBaseRef = useRef(0);
40
42
  var aviMaxRef = useRef(0);
41
43
 
44
+ var _conflictState = React.useState(0);
45
+ var conflictCount = _conflictState[0];
46
+ var setConflictCount = _conflictState[1];
47
+
48
+ var currentConflictIndexRef = React.useRef(0);
49
+
42
50
  var _useState = useState('');
43
51
  var _useState2 = _slicedToArray(_useState, 2);
44
52
  var markup = _useState2[0];
@@ -464,7 +472,7 @@ var EditorPanel = function EditorPanel(_ref) {
464
472
  language = 'ruby';break;
465
473
  default:
466
474
  switch (extension) {
467
- case 'rb':case 'ruby':case 'gemspec':
475
+ case 'rb':case 'ruby':case 'gemspec':case 'rake':
468
476
  language = 'ruby';break;
469
477
  case 'js':case 'jsx':
470
478
  language = 'javascript';break;
@@ -516,21 +524,22 @@ var EditorPanel = function EditorPanel(_ref) {
516
524
  // Evict the LRU model if the cache is at capacity before creating a new one.
517
525
  TabManager.evictLruModel();
518
526
 
519
- // Pretty-print JSON content before initial load
520
- var contentForModel = tab.content;
521
- if (language === 'json' && contentForModel) {
522
- try {
523
- contentForModel = JSON.stringify(JSON.parse(contentForModel), null, 2);
524
- } catch (_) {
525
- // invalid JSON — use raw content; Monaco will show error markers
526
- }
527
- }
528
-
529
- modelObj = window.monaco.editor.createModel(contentForModel, language);
527
+ modelObj = window.monaco.editor.createModel(tab.content, language);
530
528
  window.__mbeditorModels[tab.path] = { model: modelObj, aviBase: null, aviMax: null, lastAccessed: Date.now(), cleanVersionId: null };
531
529
  _modelEntry = window.__mbeditorModels[tab.path];
532
530
  }
533
531
 
532
+ if (typeof HistoryService !== 'undefined') {
533
+ var _histBranch = EditorStore.getState().gitBranch || '';
534
+ if (_histBranch) {
535
+ if (_reusingModel) {
536
+ HistoryService.resumeTracking(_histBranch, tab.path);
537
+ } else {
538
+ HistoryService.beginTracking(_histBranch, tab.path, tab.content || '');
539
+ }
540
+ }
541
+ }
542
+
534
543
  // Sync latestContentRef from the actual model content so the onDidChangeContent
535
544
  // handler doesn't fire a spurious onContentChange call on the first keystroke.
536
545
  latestContentRef.current = _reusingModel ? modelObj.getValue() : (tab.content || '');
@@ -563,9 +572,9 @@ var EditorPanel = function EditorPanel(_ref) {
563
572
  formatOnPaste: editorPrefs.formatOnPaste !== false,
564
573
  formatOnType: editorPrefs.formatOnType !== false,
565
574
  quickSuggestions: editorPrefs.quickSuggestions !== false,
566
- wordBasedSuggestions: editorPrefs.wordBasedSuggestions || 'matchingDocuments',
575
+ wordBasedSuggestions: editorPrefs.wordBasedSuggestions || 'currentDocument',
567
576
  acceptSuggestionOnEnter: editorPrefs.acceptSuggestionOnEnter || 'on',
568
- linkedEditing: true,
577
+ linkedEditing: !!(editorPrefs.linkedEditing),
569
578
  fixedOverflowWidgets: true,
570
579
  hover: { above: false }
571
580
  });
@@ -698,6 +707,9 @@ var EditorPanel = function EditorPanel(_ref) {
698
707
  EditorStore.setState({ canUndo: avi > aviBaseRef.current, canRedo: avi < aviMaxRef.current });
699
708
 
700
709
  var contentDisposable = modelObj.onDidChangeContent(function (e) {
710
+ if (typeof HistoryService !== 'undefined') {
711
+ HistoryService.recordOps(tab.path, e.changes);
712
+ }
701
713
  var currentAvi = modelObj.getAlternativeVersionId();
702
714
  if (!e.isUndoing && !e.isRedoing) {
703
715
  // New edit: redo stack discarded at this point, so max resets here
@@ -705,7 +717,12 @@ var EditorPanel = function EditorPanel(_ref) {
705
717
  } else if (currentAvi > aviMaxRef.current) {
706
718
  aviMaxRef.current = currentAvi;
707
719
  }
708
- EditorStore.setState({ canUndo: currentAvi > aviBaseRef.current, canRedo: currentAvi < aviMaxRef.current });
720
+ var newCanUndo = currentAvi > aviBaseRef.current;
721
+ var newCanRedo = currentAvi < aviMaxRef.current;
722
+ var _st = EditorStore.getState();
723
+ if (_st.canUndo !== newCanUndo || _st.canRedo !== newCanRedo) {
724
+ EditorStore.setState({ canUndo: newCanUndo, canRedo: newCanRedo });
725
+ }
709
726
 
710
727
  var val = editor.getValue();
711
728
 
@@ -735,6 +752,128 @@ var EditorPanel = function EditorPanel(_ref) {
735
752
  }
736
753
  });
737
754
 
755
+ // Phase 2: background undo-history replay.
756
+ // Only run for newly-created models (reused models already have their undo stack).
757
+ var _phase2CleanupFn = null;
758
+ if (!_reusingModel && typeof HistoryService !== 'undefined') {
759
+ var _phase2Branch = EditorStore.getState().gitBranch || '';
760
+ var _phase2Path = tab.path;
761
+ var _phase2Content = tab.content || '';
762
+ var _phase2Buf = [];
763
+ var _phase2Active = true;
764
+
765
+ var _phase2ModelA = modelObj;
766
+ var _phase2Listener = _phase2ModelA.onDidChangeContent(function (ev) {
767
+ if (!_phase2Active) return;
768
+ for (var _ci = 0; _ci < ev.changes.length; _ci++) {
769
+ var _c = ev.changes[_ci];
770
+ _phase2Buf.push([
771
+ _c.range.startLineNumber, _c.range.startColumn,
772
+ _c.range.endLineNumber, _c.range.endColumn,
773
+ _c.text
774
+ ]);
775
+ }
776
+ });
777
+
778
+ var _runPhase2 = function () {
779
+ if (!_phase2Active || !_phase2Branch) return;
780
+ HistoryService.fetchHistory(_phase2Branch, _phase2Path).then(function (hist) {
781
+ if (!_phase2Active) return;
782
+ if (!hist || !hist.ops || hist.ops.length === 0) {
783
+ _phase2Listener.dispose();
784
+ return;
785
+ }
786
+
787
+ var _lang = modelObj.getLanguageId();
788
+ var modelB = window.monaco.editor.createModel(hist.base, _lang);
789
+
790
+ HistoryService.setReplayInProgress(_phase2Path, true);
791
+ try {
792
+ for (var _oi = 0; _oi < hist.ops.length; _oi++) {
793
+ var _op = hist.ops[_oi];
794
+ modelB.pushEditOperations([], [{
795
+ range: new window.monaco.Range(_op[0], _op[1], _op[2], _op[3]),
796
+ text: _op[4] || ''
797
+ }], function () { return null; });
798
+ }
799
+ } catch (e) {
800
+ HistoryService.setReplayInProgress(_phase2Path, false);
801
+ _phase2Listener.dispose();
802
+ modelB.dispose();
803
+ return;
804
+ }
805
+ HistoryService.setReplayInProgress(_phase2Path, false);
806
+
807
+ var _expectedContent = _phase2ModelA.getValue();
808
+ if (modelB.getValue() !== _expectedContent) {
809
+ _phase2Listener.dispose();
810
+ modelB.dispose();
811
+ return;
812
+ }
813
+
814
+ if (_phase2Buf.length > 0) {
815
+ try {
816
+ for (var _bi = 0; _bi < _phase2Buf.length; _bi++) {
817
+ var _bop = _phase2Buf[_bi];
818
+ modelB.pushEditOperations([], [{
819
+ range: new window.monaco.Range(_bop[0], _bop[1], _bop[2], _bop[3]),
820
+ text: _bop[4] || ''
821
+ }], function () { return null; });
822
+ }
823
+ } catch (e) {
824
+ _phase2Listener.dispose();
825
+ modelB.dispose();
826
+ return;
827
+ }
828
+ }
829
+
830
+ _phase2Listener.dispose();
831
+ if (!_phase2Active) { modelB.dispose(); return; }
832
+
833
+ if (modelB.getLanguageId() !== _lang) {
834
+ window.monaco.editor.setModelLanguage(modelB, _lang);
835
+ }
836
+ modelB._mbeditorPath = _phase2Path;
837
+
838
+ var _vs = editor.saveViewState();
839
+ editor.setModel(modelB);
840
+ if (_vs) editor.restoreViewState(_vs);
841
+
842
+ var _oldEntry = window.__mbeditorModels[_phase2Path];
843
+ if (_oldEntry && _oldEntry.model !== modelB) {
844
+ var _oldModel = _oldEntry.model;
845
+ window.__mbeditorModels[_phase2Path] = {
846
+ model: modelB,
847
+ aviBase: aviBaseRef.current,
848
+ aviMax: modelB.getAlternativeVersionId(),
849
+ lastAccessed: Date.now(),
850
+ cleanVersionId: _oldEntry.cleanVersionId
851
+ };
852
+ setTimeout(function () {
853
+ if (_oldModel && !_oldModel.isDisposed()) _oldModel.dispose();
854
+ }, 0);
855
+ }
856
+
857
+ // Re-attach the HistoryService content listener to modelB so ops recorded
858
+ // after the swap are captured without waiting for the next tab switch.
859
+ var _modelBListener = modelB.onDidChangeContent(function (ev) {
860
+ HistoryService.recordOps(_phase2Path, ev.changes);
861
+ });
862
+ _phase2CleanupFn = function () { _modelBListener.dispose(); };
863
+ }).catch(function () {
864
+ _phase2Listener.dispose();
865
+ });
866
+ };
867
+
868
+ if (typeof requestIdleCallback !== 'undefined') {
869
+ requestIdleCallback(_runPhase2, { timeout: 2000 });
870
+ } else {
871
+ setTimeout(_runPhase2, 200);
872
+ }
873
+
874
+ _phase2CleanupFn = function () { _phase2Active = false; _phase2Listener.dispose(); };
875
+ }
876
+
738
877
  return function () {
739
878
  blameDecorationsRef.current = editor.deltaDecorations(blameDecorationsRef.current, []);
740
879
  testDecorationIdsRef.current = editor.deltaDecorations(testDecorationIdsRef.current, []);
@@ -775,6 +914,7 @@ var EditorPanel = function EditorPanel(_ref) {
775
914
  columnSelectDisposable.dispose();
776
915
  contentDisposable.dispose();
777
916
  EditorStore.setState({ canUndo: false, canRedo: false });
917
+ if (_phase2CleanupFn) _phase2CleanupFn();
778
918
  // Detach the model before disposing the editor so the model (and its undo
779
919
  // history) survives for when the user returns to this tab.
780
920
  editor.setModel(null);
@@ -811,18 +951,7 @@ var EditorPanel = function EditorPanel(_ref) {
811
951
  var _initEntry = window.__mbeditorModels && window.__mbeditorModels[tab.path];
812
952
  if (_initEntry) _initEntry.cleanVersionId = null;
813
953
 
814
- // Pretty-print JSON content before initial load
815
- var contentToSet = tab.content;
816
- var modelLang = model.getLanguageId();
817
- if (modelLang === 'json' && contentToSet) {
818
- try {
819
- contentToSet = JSON.stringify(JSON.parse(contentToSet), null, 2);
820
- } catch (_) {
821
- // invalid JSON — use raw content; Monaco will show error markers
822
- }
823
- }
824
-
825
- editor.setValue(contentToSet);
954
+ editor.setValue(tab.content);
826
955
  // Reset the AVI baseline: setValue clears the undo stack so anything before
827
956
  // this point is no longer reachable. Also clear the canUndo/canRedo display.
828
957
  var newBase = model.getAlternativeVersionId();
@@ -848,6 +977,54 @@ var EditorPanel = function EditorPanel(_ref) {
848
977
  }
849
978
  }, [tab.content, tab.externalContentVersion]);
850
979
 
980
+ useEffect(function () {
981
+ var editor = monacoRef.current;
982
+ if (!editor || !window.monaco || typeof tab.content !== 'string') return;
983
+ var model = editor.getModel();
984
+ if (!model) return;
985
+
986
+ if (!ConflictParser.hasConflicts(tab.content)) {
987
+ conflictDecorationsRef.current = model.deltaDecorations(conflictDecorationsRef.current, []);
988
+ conflictBlocksRef.current = [];
989
+ setConflictCount(0);
990
+ return;
991
+ }
992
+
993
+ var blocks = ConflictParser.parse(tab.content);
994
+ conflictBlocksRef.current = blocks;
995
+ setConflictCount(blocks.length);
996
+
997
+ var decorations = [];
998
+ blocks.forEach(function (block) {
999
+ decorations.push({
1000
+ range: new window.monaco.Range(block.startLine + 1, 1, block.startLine + 1, 1),
1001
+ options: { isWholeLine: true, className: 'mb-conflict-marker-line' }
1002
+ });
1003
+ if (block.headEnd > block.startLine + 1) {
1004
+ decorations.push({
1005
+ range: new window.monaco.Range(block.startLine + 2, 1, block.headEnd, 1),
1006
+ options: { isWholeLine: true, className: 'mb-conflict-head' }
1007
+ });
1008
+ }
1009
+ decorations.push({
1010
+ range: new window.monaco.Range(block.dividerLine + 1, 1, block.dividerLine + 1, 1),
1011
+ options: { isWholeLine: true, className: 'mb-conflict-marker-line' }
1012
+ });
1013
+ if (block.endLine > block.dividerLine + 1) {
1014
+ decorations.push({
1015
+ range: new window.monaco.Range(block.dividerLine + 2, 1, block.endLine, 1),
1016
+ options: { isWholeLine: true, className: 'mb-conflict-incoming' }
1017
+ });
1018
+ }
1019
+ decorations.push({
1020
+ range: new window.monaco.Range(block.endLine + 1, 1, block.endLine + 1, 1),
1021
+ options: { isWholeLine: true, className: 'mb-conflict-marker-line' }
1022
+ });
1023
+ });
1024
+
1025
+ conflictDecorationsRef.current = model.deltaDecorations(conflictDecorationsRef.current, decorations);
1026
+ }, [tab.content, tab.externalContentVersion]);
1027
+
851
1028
  // Apply editorPrefs changes to a running editor without remounting
852
1029
  useEffect(function () {
853
1030
  if (!window.monaco) return;
@@ -879,7 +1056,8 @@ var EditorPanel = function EditorPanel(_ref) {
879
1056
  formatOnPaste: editorPrefs.formatOnPaste !== false,
880
1057
  formatOnType: editorPrefs.formatOnType !== false,
881
1058
  quickSuggestions: editorPrefs.quickSuggestions !== false,
882
- wordBasedSuggestions: editorPrefs.wordBasedSuggestions || 'matchingDocuments',
1059
+ wordBasedSuggestions: editorPrefs.wordBasedSuggestions || 'currentDocument',
1060
+ linkedEditing: !!(editorPrefs.linkedEditing),
883
1061
  acceptSuggestionOnEnter: editorPrefs.acceptSuggestionOnEnter || 'on'
884
1062
  });
885
1063
  }
@@ -914,6 +1092,20 @@ var EditorPanel = function EditorPanel(_ref) {
914
1092
  });
915
1093
  MonacoVim.VimMode.Vim.map('<C-p>', ':mbeditorquickopen<CR>', 'normal');
916
1094
  MonacoVim.VimMode.Vim.map('<C-p>', ':mbeditorquickopen<CR>', 'visual');
1095
+ // :split / :vsplit — open the current file in the other pane and focus it.
1096
+ // Both map to the same behaviour since the editor always has exactly two panes.
1097
+ var openInOtherPane = function() {
1098
+ var model = monacoRef.current && monacoRef.current.getModel();
1099
+ var filePath = model && model._mbeditorPath;
1100
+ if (!filePath || typeof TabManager === 'undefined') return;
1101
+ var otherPaneId = paneId === 1 ? 2 : 1;
1102
+ // forcePaneId (4th arg) bypasses the "redirect empty pane 2 → pane 1" guard.
1103
+ TabManager.openTab(filePath, filePath.split('/').pop(), null, otherPaneId);
1104
+ TabManager.focusPane(otherPaneId);
1105
+ window.dispatchEvent(new CustomEvent('mbeditor:focusPane', { detail: { paneId: otherPaneId } }));
1106
+ };
1107
+ MonacoVim.VimMode.Vim.defineEx('split', 'sp', openInOtherPane);
1108
+ MonacoVim.VimMode.Vim.defineEx('vsplit', 'vs', openInOtherPane);
917
1109
  vimModeObjRef.current = vimInstance;
918
1110
  });
919
1111
  } else {
@@ -931,6 +1123,18 @@ var EditorPanel = function EditorPanel(_ref) {
931
1123
  };
932
1124
  }, [editorPrefs.vimMode]);
933
1125
 
1126
+ // Focus this pane's Monaco editor when MbeditorApp dispatches mbeditor:focusPane
1127
+ // (used by the vim Ctrl+W panel-switching handler to move keyboard focus).
1128
+ useEffect(function() {
1129
+ function onFocusPane(e) {
1130
+ if (e.detail && e.detail.paneId === paneId && monacoRef.current) {
1131
+ monacoRef.current.focus();
1132
+ }
1133
+ }
1134
+ window.addEventListener('mbeditor:focusPane', onFocusPane);
1135
+ return function() { window.removeEventListener('mbeditor:focusPane', onFocusPane); };
1136
+ }, [paneId]);
1137
+
934
1138
  // Jump to line if specified
935
1139
  useEffect(function () {
936
1140
  if (tab.gotoLine && monacoRef.current) {
@@ -938,7 +1142,7 @@ var EditorPanel = function EditorPanel(_ref) {
938
1142
  var editor = monacoRef.current;
939
1143
  setTimeout(function () {
940
1144
  editor.revealLineInCenter(tab.gotoLine);
941
- editor.setPosition({ lineNumber: tab.gotoLine, column: 1 });
1145
+ editor.setPosition({ lineNumber: tab.gotoLine, column: tab.gotoCol || 1 });
942
1146
  editor.focus();
943
1147
 
944
1148
  TabManager.saveTabViewState(tab.id, editor.saveViewState());
@@ -1335,7 +1539,7 @@ var EditorPanel = function EditorPanel(_ref) {
1335
1539
  var isImage = tab.isImage || IMAGE_EXTENSIONS.includes(ext);
1336
1540
  var isMarkdown = ['md', 'markdown'].includes(ext);
1337
1541
  var fileBaseName = (tab.path || '').split('/').pop().toLowerCase();
1338
- var isRubyFile = ext === 'rb' || ext === 'ruby' || ext === 'gemspec' ||
1542
+ var isRubyFile = ext === 'rb' || ext === 'ruby' || ext === 'gemspec' || ext === 'rake' ||
1339
1543
  fileBaseName === 'gemfile' || fileBaseName === 'gemfile.lock' || fileBaseName === 'rakefile';
1340
1544
 
1341
1545
  useEffect(function () {
@@ -1458,6 +1662,57 @@ var EditorPanel = function EditorPanel(_ref) {
1458
1662
  );
1459
1663
  }
1460
1664
 
1665
+ function resolveConflict(blockIndex, resolution) {
1666
+ var editor = monacoRef.current;
1667
+ if (!editor || !window.monaco) return;
1668
+ var blocks = conflictBlocksRef.current;
1669
+ if (blockIndex < 0 || blockIndex >= blocks.length) return;
1670
+ var block = blocks[blockIndex];
1671
+ var model = editor.getModel();
1672
+ if (!model) return;
1673
+
1674
+ var resolvedContent;
1675
+ if (resolution === 'head') {
1676
+ resolvedContent = block.headContent;
1677
+ } else if (resolution === 'incoming') {
1678
+ resolvedContent = block.incomingContent;
1679
+ } else {
1680
+ resolvedContent = block.headContent +
1681
+ (block.headContent && block.incomingContent ? '\n' : '') +
1682
+ block.incomingContent;
1683
+ }
1684
+
1685
+ editor.pushUndoStop();
1686
+ editor.executeEdits('conflict-resolve', [{
1687
+ range: new window.monaco.Range(
1688
+ block.startLine + 1, 1,
1689
+ block.endLine + 1,
1690
+ model.getLineMaxColumn(block.endLine + 1)
1691
+ ),
1692
+ text: resolvedContent
1693
+ }]);
1694
+ editor.pushUndoStop();
1695
+
1696
+ var newContent = editor.getValue();
1697
+ EditorStore.setState({
1698
+ panes: EditorStore.getState().panes.map(function (p) {
1699
+ return Object.assign({}, p, {
1700
+ tabs: p.tabs.map(function (t) {
1701
+ if (t.path !== tab.path) return t;
1702
+ return Object.assign({}, t, {
1703
+ content: newContent,
1704
+ dirty: true,
1705
+ externalContentVersion: (t.externalContentVersion || 0) + 1
1706
+ });
1707
+ })
1708
+ });
1709
+ })
1710
+ });
1711
+
1712
+ var remaining = conflictBlocksRef.current.length - 1;
1713
+ currentConflictIndexRef.current = Math.min(currentConflictIndexRef.current, Math.max(0, remaining - 1));
1714
+ }
1715
+
1461
1716
  // Always render the same wrapper structure so the editorRef div is never
1462
1717
  // unmounted when gitAvailable changes (e.g. loaded async after workspace
1463
1718
  // call returns). The toolbar is conditionally included inside the wrapper.
@@ -1532,6 +1787,65 @@ var EditorPanel = function EditorPanel(_ref) {
1532
1787
  !editorPrefs.toolbarIconOnly && !testLoading && React.createElement('span', { className: 'ide-toolbar-label' }, 'Test')
1533
1788
  )
1534
1789
  ),
1790
+ conflictCount > 0 && React.createElement(
1791
+ 'div', { className: 'mb-conflict-banner' },
1792
+ React.createElement(
1793
+ 'span', { className: 'mb-conflict-count' },
1794
+ React.createElement('i', { className: 'fas fa-code-merge' }),
1795
+ ' ',
1796
+ conflictCount,
1797
+ ' merge conflict',
1798
+ conflictCount !== 1 ? 's' : ''
1799
+ ),
1800
+ React.createElement(
1801
+ 'div', { className: 'mb-conflict-nav' },
1802
+ React.createElement('button', {
1803
+ className: 'mb-btn mb-btn-sm',
1804
+ title: 'Previous conflict',
1805
+ onClick: function () {
1806
+ var idx = Math.max(0, currentConflictIndexRef.current - 1);
1807
+ currentConflictIndexRef.current = idx;
1808
+ var b = conflictBlocksRef.current[idx];
1809
+ if (b && monacoRef.current) {
1810
+ monacoRef.current.revealLineInCenter(b.startLine + 1);
1811
+ monacoRef.current.setPosition({ lineNumber: b.startLine + 1, column: 1 });
1812
+ }
1813
+ }
1814
+ }, '↑ Prev'),
1815
+ React.createElement('button', {
1816
+ className: 'mb-btn mb-btn-sm',
1817
+ title: 'Next conflict',
1818
+ onClick: function () {
1819
+ var blocks = conflictBlocksRef.current;
1820
+ var idx = Math.min(blocks.length - 1, currentConflictIndexRef.current + 1);
1821
+ currentConflictIndexRef.current = idx;
1822
+ var b = blocks[idx];
1823
+ if (b && monacoRef.current) {
1824
+ monacoRef.current.revealLineInCenter(b.startLine + 1);
1825
+ monacoRef.current.setPosition({ lineNumber: b.startLine + 1, column: 1 });
1826
+ }
1827
+ }
1828
+ }, '↓ Next')
1829
+ ),
1830
+ React.createElement(
1831
+ 'div', { className: 'mb-conflict-actions' },
1832
+ React.createElement('button', {
1833
+ className: 'mb-btn mb-btn-sm mb-btn-success',
1834
+ title: 'Accept current (HEAD) — keep your local changes',
1835
+ onClick: function () { resolveConflict(currentConflictIndexRef.current, 'head'); }
1836
+ }, 'Accept Current'),
1837
+ React.createElement('button', {
1838
+ className: 'mb-btn mb-btn-sm mb-btn-incoming',
1839
+ title: 'Accept incoming — take the changes being merged in',
1840
+ onClick: function () { resolveConflict(currentConflictIndexRef.current, 'incoming'); }
1841
+ }, 'Accept Incoming'),
1842
+ React.createElement('button', {
1843
+ className: 'mb-btn mb-btn-sm',
1844
+ title: 'Accept both — keep current above incoming',
1845
+ onClick: function () { resolveConflict(currentConflictIndexRef.current, 'both'); }
1846
+ }, 'Accept Both')
1847
+ )
1848
+ ),
1535
1849
  tab.truncated && React.createElement(
1536
1850
  'div',
1537
1851
  {
@@ -1665,4 +1979,18 @@ var EditorPanel = function EditorPanel(_ref) {
1665
1979
  );
1666
1980
  };
1667
1981
 
1668
- window.EditorPanel = EditorPanel;
1982
+ // treeData and all function props (onSave, onFormat, etc.) are intentionally
1983
+ // excluded — their references change every parent render but don't affect editor output.
1984
+ window.EditorPanel = React.memo(EditorPanel, function(prev, next) {
1985
+ return prev.tab === next.tab &&
1986
+ prev.paneId === next.paneId &&
1987
+ (prev.markers === next.markers || (prev.markers.length === 0 && next.markers.length === 0)) &&
1988
+ prev.gitAvailable === next.gitAvailable &&
1989
+ prev.testAvailable === next.testAvailable &&
1990
+ prev.testResult === next.testResult &&
1991
+ prev.testPanelFile === next.testPanelFile &&
1992
+ prev.testLoading === next.testLoading &&
1993
+ prev.testInlineVisible === next.testInlineVisible &&
1994
+ prev.editorPrefs === next.editorPrefs &&
1995
+ prev.monacoReady === next.monacoReady;
1996
+ });