mbeditor 0.5.2 → 0.5.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd0837fbeccc3634d7804e6f9957bbbaca06bb4c02942eb4decc5e8b174499ab
4
- data.tar.gz: 9f97239369b2968c17cd3909abdced4917329a349564d02ebde6beb89cb8c36f
3
+ metadata.gz: 6ce79d4cc314cce5975277d04c12a0d7d5fa13629fa0447d66cf742bf05aee3e
4
+ data.tar.gz: 820ef6cac234149c3f3e69069c5255598b27d161dea7c60939d06999d16d9c4a
5
5
  SHA512:
6
- metadata.gz: 6310dc010236970bf25c0945a465122e1f8a081c399696d8870dc852d85454153863b01b1f1e9a8871abc3fbe07b1489724a065835c2909daa15426899701833
7
- data.tar.gz: f8ed953601769d6785a4fecd4c3b16a6ce820e6e7f5c88a7cb12750d8f32b5122dc6bc25fd67be5e86bd7ef02ae1beba9ece2dbc5fc5bc0324df47785a03c9ba
6
+ metadata.gz: 87cc5da11a46b54e9399ecb678c450a6f71053807a2f80bb22b788ef7c4c359b2cbcb2cc6bce4e1691763336ecd9c9ef4d9540f6a80f587f013188142a7d0b1c
7
+ data.tar.gz: 3cd29156eda196448f5f31eed27e5b8d7483e7a8f2f91d70ab6adc58a039a3e5cb19568a6b1a7bc94e38bee904cc4a495e9c698428117e8cd5aba4c09f6e9342
@@ -4,6 +4,7 @@
4
4
  //= require mbeditor/file_service
5
5
  //= require mbeditor/websocket_service
6
6
  //= require mbeditor/git_service
7
+ //= require mbeditor/conflict_parser
7
8
  //= require mbeditor/search_service
8
9
  //= require mbeditor/tab_manager
9
10
  //= require mbeditor/editor_plugins
@@ -35,10 +35,18 @@ var EditorPanel = function EditorPanel(_ref) {
35
35
  var editorRef = useRef(null);
36
36
  var monacoRef = useRef(null);
37
37
  var latestContentRef = useRef('');
38
- var lastAppliedExternalVersionRef = useRef(-1);
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];
@@ -832,13 +840,68 @@ var EditorPanel = function EditorPanel(_ref) {
832
840
  EditorStore.setState({ canUndo: false, canRedo: false });
833
841
  } else {
834
842
  // Keep undo stack for formats or replaces by using executeEdits
843
+ var _extEntry = window.__mbeditorModels && window.__mbeditorModels[tab.path];
844
+ if (_extEntry) _extEntry.cleanVersionId = null;
835
845
  editor.pushUndoStop();
836
846
  editor.executeEdits("external", [{
837
847
  range: model.getFullModelRange(),
838
848
  text: tab.content
839
849
  }]);
840
850
  editor.pushUndoStop();
851
+ // Re-anchor the clean baseline so onDidChangeContent doesn't mark this
852
+ // externally-applied content as a dirty edit.
853
+ var _newExtAvi = model.getAlternativeVersionId();
854
+ if (_extEntry) _extEntry.cleanVersionId = _newExtAvi;
855
+ aviBaseRef.current = _newExtAvi;
856
+ }
857
+ }, [tab.content, tab.externalContentVersion]);
858
+
859
+ useEffect(function () {
860
+ var editor = monacoRef.current;
861
+ if (!editor || !window.monaco || typeof tab.content !== 'string') return;
862
+ var model = editor.getModel();
863
+ if (!model) return;
864
+
865
+ if (!ConflictParser.hasConflicts(tab.content)) {
866
+ conflictDecorationsRef.current = model.deltaDecorations(conflictDecorationsRef.current, []);
867
+ conflictBlocksRef.current = [];
868
+ setConflictCount(0);
869
+ return;
841
870
  }
871
+
872
+ var blocks = ConflictParser.parse(tab.content);
873
+ conflictBlocksRef.current = blocks;
874
+ setConflictCount(blocks.length);
875
+
876
+ var decorations = [];
877
+ blocks.forEach(function (block) {
878
+ decorations.push({
879
+ range: new window.monaco.Range(block.startLine + 1, 1, block.startLine + 1, 1),
880
+ options: { isWholeLine: true, className: 'mb-conflict-marker-line' }
881
+ });
882
+ if (block.headEnd > block.startLine + 1) {
883
+ decorations.push({
884
+ range: new window.monaco.Range(block.startLine + 2, 1, block.headEnd, 1),
885
+ options: { isWholeLine: true, className: 'mb-conflict-head' }
886
+ });
887
+ }
888
+ decorations.push({
889
+ range: new window.monaco.Range(block.dividerLine + 1, 1, block.dividerLine + 1, 1),
890
+ options: { isWholeLine: true, className: 'mb-conflict-marker-line' }
891
+ });
892
+ if (block.endLine > block.dividerLine + 1) {
893
+ decorations.push({
894
+ range: new window.monaco.Range(block.dividerLine + 2, 1, block.endLine, 1),
895
+ options: { isWholeLine: true, className: 'mb-conflict-incoming' }
896
+ });
897
+ }
898
+ decorations.push({
899
+ range: new window.monaco.Range(block.endLine + 1, 1, block.endLine + 1, 1),
900
+ options: { isWholeLine: true, className: 'mb-conflict-marker-line' }
901
+ });
902
+ });
903
+
904
+ conflictDecorationsRef.current = model.deltaDecorations(conflictDecorationsRef.current, decorations);
842
905
  }, [tab.content, tab.externalContentVersion]);
843
906
 
844
907
  // Apply editorPrefs changes to a running editor without remounting
@@ -1451,6 +1514,57 @@ var EditorPanel = function EditorPanel(_ref) {
1451
1514
  );
1452
1515
  }
1453
1516
 
1517
+ function resolveConflict(blockIndex, resolution) {
1518
+ var editor = monacoRef.current;
1519
+ if (!editor || !window.monaco) return;
1520
+ var blocks = conflictBlocksRef.current;
1521
+ if (blockIndex < 0 || blockIndex >= blocks.length) return;
1522
+ var block = blocks[blockIndex];
1523
+ var model = editor.getModel();
1524
+ if (!model) return;
1525
+
1526
+ var resolvedContent;
1527
+ if (resolution === 'head') {
1528
+ resolvedContent = block.headContent;
1529
+ } else if (resolution === 'incoming') {
1530
+ resolvedContent = block.incomingContent;
1531
+ } else {
1532
+ resolvedContent = block.headContent +
1533
+ (block.headContent && block.incomingContent ? '\n' : '') +
1534
+ block.incomingContent;
1535
+ }
1536
+
1537
+ editor.pushUndoStop();
1538
+ editor.executeEdits('conflict-resolve', [{
1539
+ range: new window.monaco.Range(
1540
+ block.startLine + 1, 1,
1541
+ block.endLine + 1,
1542
+ model.getLineMaxColumn(block.endLine + 1)
1543
+ ),
1544
+ text: resolvedContent
1545
+ }]);
1546
+ editor.pushUndoStop();
1547
+
1548
+ var newContent = editor.getValue();
1549
+ EditorStore.setState({
1550
+ panes: EditorStore.getState().panes.map(function (p) {
1551
+ return Object.assign({}, p, {
1552
+ tabs: p.tabs.map(function (t) {
1553
+ if (t.path !== tab.path) return t;
1554
+ return Object.assign({}, t, {
1555
+ content: newContent,
1556
+ dirty: true,
1557
+ externalContentVersion: (t.externalContentVersion || 0) + 1
1558
+ });
1559
+ })
1560
+ });
1561
+ })
1562
+ });
1563
+
1564
+ var remaining = conflictBlocksRef.current.length - 1;
1565
+ currentConflictIndexRef.current = Math.min(currentConflictIndexRef.current, Math.max(0, remaining - 1));
1566
+ }
1567
+
1454
1568
  // Always render the same wrapper structure so the editorRef div is never
1455
1569
  // unmounted when gitAvailable changes (e.g. loaded async after workspace
1456
1570
  // call returns). The toolbar is conditionally included inside the wrapper.
@@ -1525,6 +1639,65 @@ var EditorPanel = function EditorPanel(_ref) {
1525
1639
  !editorPrefs.toolbarIconOnly && !testLoading && React.createElement('span', { className: 'ide-toolbar-label' }, 'Test')
1526
1640
  )
1527
1641
  ),
1642
+ conflictCount > 0 && React.createElement(
1643
+ 'div', { className: 'mb-conflict-banner' },
1644
+ React.createElement(
1645
+ 'span', { className: 'mb-conflict-count' },
1646
+ React.createElement('i', { className: 'fas fa-code-merge' }),
1647
+ ' ',
1648
+ conflictCount,
1649
+ ' merge conflict',
1650
+ conflictCount !== 1 ? 's' : ''
1651
+ ),
1652
+ React.createElement(
1653
+ 'div', { className: 'mb-conflict-nav' },
1654
+ React.createElement('button', {
1655
+ className: 'mb-btn mb-btn-sm',
1656
+ title: 'Previous conflict',
1657
+ onClick: function () {
1658
+ var idx = Math.max(0, currentConflictIndexRef.current - 1);
1659
+ currentConflictIndexRef.current = idx;
1660
+ var b = conflictBlocksRef.current[idx];
1661
+ if (b && monacoRef.current) {
1662
+ monacoRef.current.revealLineInCenter(b.startLine + 1);
1663
+ monacoRef.current.setPosition({ lineNumber: b.startLine + 1, column: 1 });
1664
+ }
1665
+ }
1666
+ }, '↑ Prev'),
1667
+ React.createElement('button', {
1668
+ className: 'mb-btn mb-btn-sm',
1669
+ title: 'Next conflict',
1670
+ onClick: function () {
1671
+ var blocks = conflictBlocksRef.current;
1672
+ var idx = Math.min(blocks.length - 1, currentConflictIndexRef.current + 1);
1673
+ currentConflictIndexRef.current = idx;
1674
+ var b = blocks[idx];
1675
+ if (b && monacoRef.current) {
1676
+ monacoRef.current.revealLineInCenter(b.startLine + 1);
1677
+ monacoRef.current.setPosition({ lineNumber: b.startLine + 1, column: 1 });
1678
+ }
1679
+ }
1680
+ }, '↓ Next')
1681
+ ),
1682
+ React.createElement(
1683
+ 'div', { className: 'mb-conflict-actions' },
1684
+ React.createElement('button', {
1685
+ className: 'mb-btn mb-btn-sm mb-btn-success',
1686
+ title: 'Accept current (HEAD) — keep your local changes',
1687
+ onClick: function () { resolveConflict(currentConflictIndexRef.current, 'head'); }
1688
+ }, 'Accept Current'),
1689
+ React.createElement('button', {
1690
+ className: 'mb-btn mb-btn-sm mb-btn-incoming',
1691
+ title: 'Accept incoming — take the changes being merged in',
1692
+ onClick: function () { resolveConflict(currentConflictIndexRef.current, 'incoming'); }
1693
+ }, 'Accept Incoming'),
1694
+ React.createElement('button', {
1695
+ className: 'mb-btn mb-btn-sm',
1696
+ title: 'Accept both — keep current above incoming',
1697
+ onClick: function () { resolveConflict(currentConflictIndexRef.current, 'both'); }
1698
+ }, 'Accept Both')
1699
+ )
1700
+ ),
1528
1701
  tab.truncated && React.createElement(
1529
1702
  'div',
1530
1703
  {
@@ -111,6 +111,44 @@ var SectionActionGroup = function SectionActionGroup(_ref2) {
111
111
  );
112
112
  };
113
113
 
114
+ function FileReloadBanner(_ref) {
115
+ var pendingReloads = _ref.pendingReloads;
116
+ var onSaveAndReload = _ref.onSaveAndReload;
117
+ var onDiscardAndReload = _ref.onDiscardAndReload;
118
+ var onKeepMine = _ref.onKeepMine;
119
+ if (!pendingReloads || pendingReloads.length === 0) return null;
120
+ return React.createElement(
121
+ 'div', { className: 'mb-file-reload-banner' },
122
+ pendingReloads.map(function (r) {
123
+ return React.createElement(
124
+ 'div', { key: r.paneId + ':' + r.tabId, className: 'mb-file-reload-item' },
125
+ React.createElement(
126
+ 'span', { className: 'mb-file-reload-msg' },
127
+ React.createElement('i', { className: 'fas fa-sync-alt' }),
128
+ ' ',
129
+ React.createElement('strong', null, r.name),
130
+ ' was updated externally'
131
+ ),
132
+ React.createElement(
133
+ 'div', { className: 'mb-file-reload-actions' },
134
+ React.createElement('button', {
135
+ className: 'mb-btn mb-btn-sm mb-btn-primary',
136
+ onClick: function () { onSaveAndReload(r); }
137
+ }, 'Save & Reload'),
138
+ React.createElement('button', {
139
+ className: 'mb-btn mb-btn-sm mb-btn-warning',
140
+ onClick: function () { onDiscardAndReload(r); }
141
+ }, 'Discard & Reload'),
142
+ React.createElement('button', {
143
+ className: 'mb-btn mb-btn-sm',
144
+ onClick: function () { onKeepMine(r); }
145
+ }, 'Keep Mine')
146
+ )
147
+ );
148
+ })
149
+ );
150
+ }
151
+
114
152
  var MbeditorApp = function MbeditorApp() {
115
153
  var _useState = useState(EditorStore.getState());
116
154
 
@@ -1099,12 +1137,77 @@ var MbeditorApp = function MbeditorApp() {
1099
1137
  SearchService.buildIndex(newData);
1100
1138
  return newData;
1101
1139
  });
1140
+ checkOpenTabsForExternalChanges();
1102
1141
  })["catch"](function () {});
1103
1142
  }
1104
1143
  WebSocketService.onFilesChanged(handleFilesChanged);
1105
1144
  return function () { WebSocketService.offFilesChanged(handleFilesChanged); };
1106
1145
  }, []);
1107
1146
 
1147
+ function checkOpenTabsForExternalChanges() {
1148
+ var st = EditorStore.getState();
1149
+ var allTabs = st.panes.reduce(function (acc, p) {
1150
+ return acc.concat(p.tabs.map(function (t) { return { paneId: p.id, tab: t }; }));
1151
+ }, []);
1152
+ var fileTabs = allTabs.filter(function (pt) {
1153
+ var path = pt.tab.path || '';
1154
+ return path &&
1155
+ !path.startsWith('mbeditor://') &&
1156
+ !path.startsWith('diff://') &&
1157
+ !path.startsWith('combined-diff://') &&
1158
+ !pt.tab.isCombinedDiff &&
1159
+ !pt.tab.isSettings &&
1160
+ !pt.tab.isImage &&
1161
+ !pt.tab.isDiff &&
1162
+ typeof pt.tab.content === 'string';
1163
+ });
1164
+ fileTabs.forEach(function (pt) {
1165
+ FileService.getFile(pt.tab.path, { allowMissing: true }).then(function (data) {
1166
+ if (!data || typeof data.content !== 'string') return;
1167
+ var serverNorm = data.content.replace(/\r\n/g, '\n');
1168
+ var tabNorm = (pt.tab.content || '').replace(/\r\n/g, '\n');
1169
+ if (serverNorm === tabNorm) return;
1170
+ if (!pt.tab.dirty) {
1171
+ EditorStore.setState({
1172
+ panes: EditorStore.getState().panes.map(function (p) {
1173
+ if (p.id !== pt.paneId) return p;
1174
+ return Object.assign({}, p, {
1175
+ tabs: p.tabs.map(function (t) {
1176
+ if (t.id !== pt.tab.id) return t;
1177
+ return Object.assign({}, t, {
1178
+ content: data.content,
1179
+ externalContentVersion: (t.externalContentVersion || 0) + 1
1180
+ });
1181
+ })
1182
+ });
1183
+ })
1184
+ });
1185
+ } else {
1186
+ // Re-verify the tab still exists before queuing
1187
+ var currentState = EditorStore.getState();
1188
+ var stillExists = currentState.panes.some(function (p) {
1189
+ return p.id === pt.paneId && p.tabs.some(function (t) { return t.id === pt.tab.id; });
1190
+ });
1191
+ if (!stillExists) return;
1192
+ var existing = EditorStore.getState().pendingReloads.find(function (r) {
1193
+ return r.paneId === pt.paneId && r.tabId === pt.tab.id;
1194
+ });
1195
+ if (!existing) {
1196
+ EditorStore.setState({
1197
+ pendingReloads: EditorStore.getState().pendingReloads.concat([{
1198
+ paneId: pt.paneId,
1199
+ tabId: pt.tab.id,
1200
+ path: pt.tab.path,
1201
+ name: pt.tab.name,
1202
+ serverContent: data.content
1203
+ }])
1204
+ });
1205
+ }
1206
+ }
1207
+ })["catch"](function () {});
1208
+ });
1209
+ }
1210
+
1108
1211
  // Auto-refresh the file tree every 10s to pick up external changes (new files, deletions, etc.)
1109
1212
  // When an ActionCable WebSocket is connected this acts only as a safety-net fallback —
1110
1213
  // the WebSocket push above handles immediate invalidation after mbeditor mutations.
@@ -1168,6 +1271,13 @@ var MbeditorApp = function MbeditorApp() {
1168
1271
  setClosingTabId(id);
1169
1272
  } else {
1170
1273
  TabManager.closeTab(paneId, id);
1274
+ EditorStore.setState({
1275
+ pendingReloads: EditorStore.getState().pendingReloads.filter(function (r) {
1276
+ return EditorStore.getState().panes.some(function (p) {
1277
+ return p.tabs.some(function (t) { return t.id === r.tabId; });
1278
+ });
1279
+ })
1280
+ });
1171
1281
  }
1172
1282
  };
1173
1283
 
@@ -1199,6 +1309,13 @@ var MbeditorApp = function MbeditorApp() {
1199
1309
  _closeEntry.cleanVersionId = _closeEntry.model.getAlternativeVersionId();
1200
1310
  }
1201
1311
  TabManager.closeTab(closingPaneId, tab.id);
1312
+ EditorStore.setState({
1313
+ pendingReloads: EditorStore.getState().pendingReloads.filter(function (r) {
1314
+ return EditorStore.getState().panes.some(function (p) {
1315
+ return p.tabs.some(function (t) { return t.id === r.tabId; });
1316
+ });
1317
+ })
1318
+ });
1202
1319
  })["catch"](function (err) {
1203
1320
  EditorStore.setStatus("Save failed: " + err.message, "error");
1204
1321
  })["finally"](function () {
@@ -1210,6 +1327,13 @@ var MbeditorApp = function MbeditorApp() {
1210
1327
  });
1211
1328
  } else {
1212
1329
  TabManager.closeTab(closingPaneId, tab.id);
1330
+ EditorStore.setState({
1331
+ pendingReloads: EditorStore.getState().pendingReloads.filter(function (r) {
1332
+ return EditorStore.getState().panes.some(function (p) {
1333
+ return p.tabs.some(function (t) { return t.id === r.tabId; });
1334
+ });
1335
+ })
1336
+ });
1213
1337
  setClosingTabId(null);
1214
1338
  setClosingPaneId(null);
1215
1339
  }
@@ -1472,6 +1596,64 @@ var MbeditorApp = function MbeditorApp() {
1472
1596
  });
1473
1597
  };
1474
1598
 
1599
+ function dismissPendingReload(reload) {
1600
+ EditorStore.setState({
1601
+ pendingReloads: EditorStore.getState().pendingReloads.filter(function (r) {
1602
+ return !(r.paneId === reload.paneId && r.tabId === reload.tabId);
1603
+ })
1604
+ });
1605
+ }
1606
+
1607
+ function handleSaveAndReload(reload) {
1608
+ var st = EditorStore.getState();
1609
+ var pane = st.panes.find(function (p) { return p.id === reload.paneId; });
1610
+ var tab = pane && pane.tabs.find(function (t) { return t.id === reload.tabId; });
1611
+ if (!tab) { dismissPendingReload(reload); return; }
1612
+ FileService.saveFile(tab.path, tab.content).then(function () {
1613
+ EditorStore.setState({
1614
+ panes: EditorStore.getState().panes.map(function (p) {
1615
+ if (p.id !== reload.paneId) return p;
1616
+ return Object.assign({}, p, {
1617
+ tabs: p.tabs.map(function (t) {
1618
+ if (t.id !== reload.tabId) return t;
1619
+ return Object.assign({}, t, {
1620
+ content: tab.content,
1621
+ dirty: false,
1622
+ externalContentVersion: (t.externalContentVersion || 0) + 1
1623
+ });
1624
+ })
1625
+ });
1626
+ })
1627
+ });
1628
+ dismissPendingReload(reload);
1629
+ })["catch"](function () {
1630
+ EditorStore.setStatus('Save failed — cannot reload', 'error');
1631
+ });
1632
+ }
1633
+
1634
+ function handleDiscardAndReload(reload) {
1635
+ EditorStore.setState({
1636
+ panes: EditorStore.getState().panes.map(function (p) {
1637
+ if (p.id !== reload.paneId) return p;
1638
+ return Object.assign({}, p, {
1639
+ tabs: p.tabs.map(function (t) {
1640
+ if (t.id !== reload.tabId) return t;
1641
+ return Object.assign({}, t, {
1642
+ content: reload.serverContent,
1643
+ dirty: false,
1644
+ externalContentVersion: (t.externalContentVersion || 0) + 1
1645
+ });
1646
+ })
1647
+ });
1648
+ })
1649
+ });
1650
+ dismissPendingReload(reload);
1651
+ }
1652
+
1653
+ function handleKeepMine(reload) {
1654
+ dismissPendingReload(reload);
1655
+ }
1656
+
1475
1657
  var handleSaveAll = function handleSaveAll() {
1476
1658
  var dirtyTabs = state.panes.flatMap(function (p) {
1477
1659
  return p.tabs;
@@ -3775,6 +3957,12 @@ var MbeditorApp = function MbeditorApp() {
3775
3957
  React.Fragment,
3776
3958
  null,
3777
3959
  renderTabBar(pane.id, pane.tabs, pane.activeTabId),
3960
+ React.createElement(FileReloadBanner, {
3961
+ pendingReloads: (state.pendingReloads || []).filter(function (r) { return r.paneId === pane.id; }),
3962
+ onSaveAndReload: handleSaveAndReload,
3963
+ onDiscardAndReload: handleDiscardAndReload,
3964
+ onKeepMine: handleKeepMine
3965
+ }),
3778
3966
  React.createElement(
3779
3967
  "div",
3780
3968
  { style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', visibility: activeResizeMode === 'pane' ? 'hidden' : 'visible' } },
@@ -0,0 +1,48 @@
1
+ var ConflictParser = (function () {
2
+ function hasConflicts(content) {
3
+ return /^<<<<<<< /m.test(content);
4
+ }
5
+
6
+ function parse(content) {
7
+ var blocks = [];
8
+ var lines = content.split('\n');
9
+ var state = null;
10
+ var headStart = -1, headEnd = -1, dividerLine = -1;
11
+ var headLines = [], incomingLines = [];
12
+ var marker = null;
13
+
14
+ for (var i = 0; i < lines.length; i++) {
15
+ var line = lines[i];
16
+ if (/^<<<<<<< /.test(line) && state === null) {
17
+ state = 'head';
18
+ headStart = i;
19
+ headLines = [];
20
+ marker = line;
21
+ } else if (/^=======\s*$/.test(line) && state === 'head') {
22
+ state = 'incoming';
23
+ headEnd = i;
24
+ dividerLine = i;
25
+ incomingLines = [];
26
+ } else if (/^>>>>>>> /.test(line) && state === 'incoming') {
27
+ blocks.push({
28
+ startLine: headStart,
29
+ headEnd: headEnd,
30
+ dividerLine: dividerLine,
31
+ endLine: i,
32
+ headContent: headLines.join('\n'),
33
+ incomingContent: incomingLines.join('\n'),
34
+ marker: marker,
35
+ endMarker: line
36
+ });
37
+ state = null;
38
+ } else if (state === 'head') {
39
+ headLines.push(line);
40
+ } else if (state === 'incoming') {
41
+ incomingLines.push(line);
42
+ }
43
+ }
44
+ return blocks;
45
+ }
46
+
47
+ return { hasConflicts: hasConflicts, parse: parse };
48
+ })();
@@ -68,9 +68,6 @@
68
68
  var key = keys[i];
69
69
  if (alreadyDeclared[key]) continue;
70
70
  if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)) continue;
71
- var desc;
72
- try { desc = Object.getOwnPropertyDescriptor(window, key); } catch (e) { continue; }
73
- if (!desc || !desc.configurable || !desc.writable || desc.get) continue;
74
71
  var value;
75
72
  try { value = window[key]; } catch (e) { continue; }
76
73
  if (value === null || value === undefined) continue;
@@ -488,11 +485,11 @@
488
485
  });
489
486
  }
490
487
 
491
- // Declare globals that the sprockets asset pipeline injects at runtime so
492
- // checkJs doesn't flag them as undefined. `interface Window` augmentation
493
- // covers `window.myAppGlobal` access patterns. For app-specific component
494
- // names not listed here, add `/* global MyComponent */` at the top of the
495
- // file TypeScript's checkJs mode respects that directive.
488
+ // Declare globals that are injected at runtime so checkJs doesn't flag them
489
+ // as undefined. The buildWindowGlobalsShim() function automatically detects
490
+ // window globals from the host application. Common Sprockets globals
491
+ // (React, ReactDOM, etc.) are declared explicitly. For additional globals
492
+ // not auto-detected, add `/* global MyComponent */` at the top of the file.
496
493
  if (monaco.languages.typescript && monaco.languages.typescript.javascriptDefaults) {
497
494
  monaco.languages.typescript.javascriptDefaults.addExtraLib(
498
495
  [
@@ -515,12 +512,18 @@
515
512
  );
516
513
  }
517
514
 
518
- // Downgrade "declared but never read" (TS6133) from Error to Warning.
519
- // TypeScript has no built-in way to emit this as a warning, so we intercept
515
+ // Downgrade certain TypeScript diagnostic codes from Error to Warning.
516
+ // TypeScript has no built-in way to emit these as warnings, so we intercept
520
517
  // the marker set after the worker fires and re-apply with lower severity.
521
- // JS files use owner 'javascript', TS files use 'typescript'.
522
- var WARN_CODES = { '6133': true };
523
- var TS_OWNERS = ['javascript', 'typescript'];
518
+ //
519
+ // Patch markers after the TypeScript worker fires:
520
+ // - Both: downgrade TS6133 ("declared but never read") from Error to Warning.
521
+ // Host-app globals are handled by the dynamic window shim and explicit
522
+ // addExtraLib declarations above — we do not suppress TS2304 globally so
523
+ // that genuinely undefined names are still flagged as errors.
524
+ var JS_SUPPRESS_CODES = {};
525
+ var JS_WARN_CODES = { '6133': true };
526
+ var TS_WARN_CODES = { '6133': true };
524
527
  var _severityPatchActive = false;
525
528
  monaco.editor.onDidChangeMarkers(function(uris) {
526
529
  if (_severityPatchActive) return;
@@ -529,14 +532,20 @@
529
532
  uris.forEach(function(uri) {
530
533
  var model = monaco.editor.getModel(uri);
531
534
  if (!model) return;
532
- TS_OWNERS.forEach(function(owner) {
533
- var markers = monaco.editor.getModelMarkers({ resource: uri, owner: owner });
535
+ [
536
+ { owner: 'javascript', suppress: JS_SUPPRESS_CODES, warn: JS_WARN_CODES },
537
+ { owner: 'typescript', suppress: {}, warn: TS_WARN_CODES }
538
+ ].forEach(function(entry) {
539
+ var markers = monaco.editor.getModelMarkers({ resource: uri, owner: entry.owner });
534
540
  var needsPatch = markers.some(function(m) {
535
- return m.severity === monaco.MarkerSeverity.Error && WARN_CODES[String(m.code)];
541
+ var code = String(m.code);
542
+ return (m.severity === monaco.MarkerSeverity.Error && (entry.suppress[code] || entry.warn[code]));
536
543
  });
537
544
  if (!needsPatch) return;
538
- monaco.editor.setModelMarkers(model, owner, markers.map(function(m) {
539
- return (m.severity === monaco.MarkerSeverity.Error && WARN_CODES[String(m.code)])
545
+ monaco.editor.setModelMarkers(model, entry.owner, markers.filter(function(m) {
546
+ return !entry.suppress[String(m.code)];
547
+ }).map(function(m) {
548
+ return (m.severity === monaco.MarkerSeverity.Error && entry.warn[String(m.code)])
540
549
  ? Object.assign({}, m, { severity: monaco.MarkerSeverity.Warning })
541
550
  : m;
542
551
  }));
@@ -548,7 +557,7 @@
548
557
  });
549
558
  }
550
559
 
551
- // TypeScript: enable JSX for .tsx files and catch unused locals.
560
+ // TypeScript: enable JSX for .tsx files.
552
561
  if (monaco.languages.typescript && monaco.languages.typescript.typescriptDefaults) {
553
562
  monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
554
563
  target: monaco.languages.typescript.ScriptTarget.ES2020,
@@ -15,6 +15,7 @@ var EditorStore = (function () {
15
15
  searchResults: [],
16
16
  searchCapped: false,
17
17
  statusMessage: { text: "", kind: "info" },
18
+ pendingReloads: [],
18
19
  canUndo: false,
19
20
  canRedo: false,
20
21
  };
@@ -861,3 +861,115 @@
861
861
  }
862
862
 
863
863
  .cdiff-ctx { color: var(--ide-text-muted); }
864
+
865
+ /* ── File Reload Banner ─────────────────────────────────────────── */
866
+ .mb-file-reload-banner {
867
+ display: flex;
868
+ flex-direction: column;
869
+ gap: 0;
870
+ flex-shrink: 0;
871
+ }
872
+
873
+ .mb-file-reload-item {
874
+ display: flex;
875
+ align-items: center;
876
+ justify-content: space-between;
877
+ padding: 5px 12px;
878
+ background: #2a2600;
879
+ border-bottom: 1px solid #5a5000;
880
+ font-size: 12px;
881
+ color: #e3d286;
882
+ gap: 12px;
883
+ }
884
+
885
+ .mb-file-reload-msg {
886
+ display: flex;
887
+ align-items: center;
888
+ gap: 6px;
889
+ flex: 1;
890
+ min-width: 0;
891
+ overflow: hidden;
892
+ text-overflow: ellipsis;
893
+ white-space: nowrap;
894
+ }
895
+
896
+ .mb-file-reload-actions {
897
+ display: flex;
898
+ gap: 5px;
899
+ flex-shrink: 0;
900
+ }
901
+
902
+ .mb-btn-warning {
903
+ background: #6b4500;
904
+ color: #ffd88a;
905
+ border: 1px solid #8a5900;
906
+ }
907
+
908
+ .mb-btn-warning:hover {
909
+ background: #8a5900;
910
+ }
911
+
912
+ /* ── Merge Conflict Decorations ─────────────────────────────────── */
913
+ .mb-conflict-marker-line {
914
+ background: rgba(120, 80, 0, 0.35) !important;
915
+ }
916
+
917
+ .mb-conflict-head {
918
+ background: rgba(180, 60, 60, 0.18) !important;
919
+ border-left: 2px solid rgba(200, 80, 80, 0.6);
920
+ }
921
+
922
+ .mb-conflict-incoming {
923
+ background: rgba(60, 130, 60, 0.18) !important;
924
+ border-left: 2px solid rgba(80, 160, 80, 0.6);
925
+ }
926
+
927
+ /* ── Conflict Banner ─────────────────────────────────────────────── */
928
+ .mb-conflict-banner {
929
+ display: flex;
930
+ align-items: center;
931
+ gap: 10px;
932
+ padding: 5px 12px;
933
+ background: #1e1a00;
934
+ border-bottom: 1px solid #5a5000;
935
+ font-size: 12px;
936
+ color: #e3d286;
937
+ flex-shrink: 0;
938
+ }
939
+
940
+ .mb-conflict-count {
941
+ font-weight: bold;
942
+ color: #f5a623;
943
+ white-space: nowrap;
944
+ }
945
+
946
+ .mb-conflict-nav {
947
+ display: flex;
948
+ gap: 4px;
949
+ }
950
+
951
+ .mb-conflict-actions {
952
+ display: flex;
953
+ gap: 5px;
954
+ margin-left: auto;
955
+ }
956
+
957
+ .mb-btn-success {
958
+ background: #1a5c2a;
959
+ color: #aef;
960
+ border: 1px solid #2a7a3a;
961
+ }
962
+
963
+ .mb-btn-success:hover {
964
+ background: #1e7a38;
965
+ }
966
+
967
+ .mb-btn-incoming {
968
+ background: #1a3060;
969
+ color: #aef;
970
+ border: 1px solid #1a4080;
971
+ }
972
+
973
+ .mb-btn-incoming:hover {
974
+ background: #1a4080;
975
+ }
@@ -36,6 +36,15 @@ module Mbeditor
36
36
  private
37
37
 
38
38
  def editor_shell_html(base)
39
+ prettier_scripts = %w[
40
+ prettier-standalone.js
41
+ prettier-plugin-babel.js
42
+ prettier-plugin-estree.js
43
+ prettier-plugin-html.js
44
+ prettier-plugin-postcss.js
45
+ prettier-plugin-markdown.js
46
+ ].map { |f| "#{base}/assets/#{f}" }.to_json
47
+
39
48
  <<~HTML
40
49
  <!DOCTYPE html>
41
50
  <html lang="en">
@@ -45,6 +54,16 @@ module Mbeditor
45
54
  <title>Mbeditor</title>
46
55
  <link rel="stylesheet" href="#{base}/assets/fontawesome.min.css" />
47
56
  <link rel="stylesheet" href="#{base}/assets/mbeditor/application.css" />
57
+ <script defer src="#{base}/assets/react.min.js"></script>
58
+ <script defer src="#{base}/assets/react-dom.min.js"></script>
59
+ <script defer src="#{base}/assets/axios.min.js"></script>
60
+ <script defer src="#{base}/assets/lodash.min.js"></script>
61
+ <script defer src="#{base}/assets/minisearch.min.js"></script>
62
+ <script defer src="#{base}/assets/marked.min.js"></script>
63
+ <script defer src="#{base}/assets/emmet.js"></script>
64
+ <script defer src="#{base}/assets/monaco-themes-bundle.js"></script>
65
+ <script>var require = { paths: { vs: '#{base}/monaco-editor/vs', 'monaco-editor/esm/vs': '#{base}/monaco-editor/vs', 'monaco-vim': '#{base}/assets/monaco-vim' } };</script>
66
+ <script src="#{base}/monaco-editor/vs/loader.js"></script>
48
67
  </head>
49
68
  <body>
50
69
  <script>
@@ -60,68 +79,70 @@ module Mbeditor
60
79
  <div class="mbeditor-loading-text">Loading editor&hellip;</div>
61
80
  </div>
62
81
  </div>
63
- <script defer src="#{base}/assets/react.min.js"></script>
64
- <script defer src="#{base}/assets/react-dom.min.js"></script>
65
- <script defer src="#{base}/assets/axios.min.js"></script>
66
- <script defer src="#{base}/assets/lodash.min.js"></script>
67
- <script defer src="#{base}/assets/minisearch.min.js"></script>
68
- <script defer src="#{base}/assets/marked.min.js"></script>
69
- <script defer src="#{base}/assets/emmet.js"></script>
70
- <script defer src="#{base}/assets/monaco-themes-bundle.js"></script>
71
82
  <script>
72
83
  window.MonacoEnvironment = {
73
84
  getWorkerUrl: function(workerId, label) {
74
- if (label === 'typescript' || label === 'javascript') return '#{base}/ts_worker.js';
75
- return '#{base}/monaco_worker.js';
85
+ var b = window.MBEDITOR_BASE_PATH || '';
86
+ if (label === 'typescript' || label === 'javascript') return b + '/ts_worker.js';
87
+ return b + '/monaco_worker.js';
76
88
  }
77
89
  };
78
- var require = { paths: { vs: '#{base}/monaco-editor/vs', 'monaco-editor/esm/vs': '#{base}/monaco-editor/vs', 'monaco-vim': '#{base}/assets/monaco-vim' } };
79
- </script>
80
- <script src="#{base}/monaco-editor/vs/loader.js"></script>
81
- <script>
82
90
  (function() {
83
- var prettierScripts = [
84
- '#{base}/assets/prettier-standalone.js',
85
- '#{base}/assets/prettier-plugin-babel.js',
86
- '#{base}/assets/prettier-plugin-estree.js',
87
- '#{base}/assets/prettier-plugin-html.js',
88
- '#{base}/assets/prettier-plugin-postcss.js',
89
- '#{base}/assets/prettier-plugin-markdown.js'
90
- ];
91
91
  var _define = window.define;
92
92
  window.define = undefined;
93
- var pending = prettierScripts.length;
94
- function onAllPrettierLoaded() {
95
- function proceed() {
96
- window.MbeditorRuntime = { React: window.React, ReactDOM: window.ReactDOM };
97
- window.React = window._mbeditorHostReact;
98
- window.ReactDOM = window._mbeditorHostReactDOM;
99
- window.define = _define;
100
- require(['vs/editor/editor.main'], function() {
101
- if (window.MBEDITOR_CUSTOM_THEMES && window.monaco) {
102
- Object.keys(window.MBEDITOR_CUSTOM_THEMES).forEach(function(id) {
103
- window.monaco.editor.defineTheme(id, window.MBEDITOR_CUSTOM_THEMES[id]);
104
- });
105
- }
93
+
94
+ var prettierScripts = #{prettier_scripts};
95
+ window.loadPrettierPlugins = function() {
96
+ if (window._prettierLoadPromise) return window._prettierLoadPromise;
97
+ window._prettierLoadPromise = new Promise(function(resolve, reject) {
98
+ var savedDefine = window.define;
99
+ window.define = undefined;
100
+ var pending = prettierScripts.length;
101
+ prettierScripts.forEach(function(src) {
106
102
  var s = document.createElement('script');
107
- s.src = '#{base}/assets/mbeditor/application.js';
108
- s.onload = function() {
109
- var root = document.getElementById('mbeditor-root');
110
- var _R = window.MbeditorRuntime.React, _RD = window.MbeditorRuntime.ReactDOM;
111
- if (window.MbeditorApp && _R && _RD) _RD.render(_R.createElement(window.MbeditorApp), root);
112
- };
113
- document.body.appendChild(s);
103
+ s.src = src;
104
+ s.onload = function() { if (--pending === 0) { window.define = savedDefine; resolve(); } };
105
+ s.onerror = function() { window.define = savedDefine; reject(new Error('Failed to load Prettier: ' + src)); };
106
+ document.head.appendChild(s);
114
107
  });
115
- }
116
- if (window._mbeditorDOMReady) proceed();
117
- else document.addEventListener('DOMContentLoaded', proceed, { once: true });
108
+ });
109
+ return window._prettierLoadPromise;
110
+ };
111
+
112
+ function proceed() {
113
+ window.MbeditorRuntime = { React: window.React, ReactDOM: window.ReactDOM };
114
+ window.React = window._mbeditorHostReact;
115
+ window.ReactDOM = window._mbeditorHostReactDOM;
116
+ window.define = _define;
117
+
118
+ var _monacoResolve;
119
+ window.__monacoReady = new Promise(function(resolve) { _monacoResolve = resolve; });
120
+
121
+ var appScript = document.createElement('script');
122
+ appScript.src = '#{base}/assets/mbeditor/application.js';
123
+ appScript.onload = function() {
124
+ var root = document.getElementById('mbeditor-root');
125
+ var _R = window.MbeditorRuntime.React, _RD = window.MbeditorRuntime.ReactDOM;
126
+ if (window.MbeditorApp && _R && _RD) _RD.render(_R.createElement(window.MbeditorApp), root);
127
+ };
128
+ appScript.onerror = function() {
129
+ document.getElementById('mbeditor-root').innerHTML =
130
+ '<div style="padding:2rem;font-family:sans-serif;color:#c00">Editor failed to load. Please refresh the page.</div>';
131
+ };
132
+ document.body.appendChild(appScript);
133
+
134
+ require(['vs/editor/editor.main'], function() {
135
+ if (window.MBEDITOR_CUSTOM_THEMES && window.monaco) {
136
+ Object.keys(window.MBEDITOR_CUSTOM_THEMES).forEach(function(id) {
137
+ window.monaco.editor.defineTheme(id, window.MBEDITOR_CUSTOM_THEMES[id]);
138
+ });
139
+ }
140
+ _monacoResolve();
141
+ });
118
142
  }
119
- prettierScripts.forEach(function(src) {
120
- var s = document.createElement('script');
121
- s.src = src;
122
- s.onload = function() { if (--pending === 0) onAllPrettierLoaded(); };
123
- document.head.appendChild(s);
124
- });
143
+
144
+ if (window._mbeditorDOMReady) proceed();
145
+ else document.addEventListener('DOMContentLoaded', proceed, { once: true });
125
146
  })();
126
147
  </script>
127
148
  </body>
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mbeditor
4
- VERSION = "0.5.2"
4
+ VERSION = "0.5.4"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mbeditor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.2
4
+ version: 0.5.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oliver Noonan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-30 00:00:00.000000000 Z
11
+ date: 2026-05-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -71,6 +71,7 @@ files:
71
71
  - app/assets/javascripts/mbeditor/components/ShortcutHelp.js
72
72
  - app/assets/javascripts/mbeditor/components/TabBar.js
73
73
  - app/assets/javascripts/mbeditor/components/TestResultsPanel.js
74
+ - app/assets/javascripts/mbeditor/conflict_parser.js
74
75
  - app/assets/javascripts/mbeditor/editor_plugins.js
75
76
  - app/assets/javascripts/mbeditor/editor_store.js
76
77
  - app/assets/javascripts/mbeditor/file_icon.js