mbeditor 0.5.3 → 0.5.5
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 +4 -4
- data/app/assets/javascripts/mbeditor/application.js +1 -0
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +166 -0
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +218 -0
- data/app/assets/javascripts/mbeditor/conflict_parser.js +48 -0
- data/app/assets/javascripts/mbeditor/editor_store.js +1 -0
- data/app/assets/javascripts/mbeditor/search_service.js +1 -0
- data/app/assets/stylesheets/mbeditor/application.css +112 -0
- data/app/controllers/mbeditor/editors_controller.rb +8 -3
- data/app/views/layouts/mbeditor/application.html.erb +2 -2
- data/lib/mbeditor/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 041ce3cd1e0cc69e9c9d4fbf4a3578d8681ce4ed54cbaf6351f36e4642d161b9
|
|
4
|
+
data.tar.gz: e65f221f2bad0d72957225ba5836104878e27865c4e86b82b45857509922d571
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9f51620ece5851894a862bc5ed080bb0a4d73a4263a987b069b212ed15d705f3a2ce1383dcb6850578bd2bb6f1aa5a00734132d11ab62cf91cfb945cf8042a5d
|
|
7
|
+
data.tar.gz: da12cdf3ddceaf8d91f173ca11df458e88f4c294294eef005401a6b2ecc3457d5d76f4ce9bb148d6fbd18e572a0b669f154ef4675855ebb170fe63859aef82dd
|
|
@@ -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];
|
|
@@ -848,6 +856,54 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
848
856
|
}
|
|
849
857
|
}, [tab.content, tab.externalContentVersion]);
|
|
850
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;
|
|
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);
|
|
905
|
+
}, [tab.content, tab.externalContentVersion]);
|
|
906
|
+
|
|
851
907
|
// Apply editorPrefs changes to a running editor without remounting
|
|
852
908
|
useEffect(function () {
|
|
853
909
|
if (!window.monaco) return;
|
|
@@ -1458,6 +1514,57 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1458
1514
|
);
|
|
1459
1515
|
}
|
|
1460
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
|
+
|
|
1461
1568
|
// Always render the same wrapper structure so the editorRef div is never
|
|
1462
1569
|
// unmounted when gitAvailable changes (e.g. loaded async after workspace
|
|
1463
1570
|
// call returns). The toolbar is conditionally included inside the wrapper.
|
|
@@ -1532,6 +1639,65 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1532
1639
|
!editorPrefs.toolbarIconOnly && !testLoading && React.createElement('span', { className: 'ide-toolbar-label' }, 'Test')
|
|
1533
1640
|
)
|
|
1534
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
|
+
),
|
|
1535
1701
|
tab.truncated && React.createElement(
|
|
1536
1702
|
'div',
|
|
1537
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;
|
|
@@ -1558,6 +1740,29 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1558
1740
|
EditorStore.setStatus('Line endings changed to ' + newEOL, 'info');
|
|
1559
1741
|
};
|
|
1560
1742
|
|
|
1743
|
+
var handleRefreshWorkspace = function handleRefreshWorkspace() {
|
|
1744
|
+
setLoading(function (prev) {
|
|
1745
|
+
return _extends({}, prev, { refreshWorkspace: true });
|
|
1746
|
+
});
|
|
1747
|
+
GitService.fetchStatus()["catch"](function () {});
|
|
1748
|
+
FileService.getTree().then(function (data) {
|
|
1749
|
+
var newData = data || [];
|
|
1750
|
+
setTreeData(function (prevData) {
|
|
1751
|
+
if (JSON.stringify(newData) === JSON.stringify(prevData)) return prevData;
|
|
1752
|
+
SearchService.buildIndex(newData);
|
|
1753
|
+
return newData;
|
|
1754
|
+
});
|
|
1755
|
+
checkOpenTabsForExternalChanges();
|
|
1756
|
+
EditorStore.setStatus("Workspace refreshed", "success");
|
|
1757
|
+
})["catch"](function (err) {
|
|
1758
|
+
EditorStore.setStatus("Failed to refresh workspace", "error");
|
|
1759
|
+
})["finally"](function () {
|
|
1760
|
+
setLoading(function (prev) {
|
|
1761
|
+
return _extends({}, prev, { refreshWorkspace: false });
|
|
1762
|
+
});
|
|
1763
|
+
});
|
|
1764
|
+
};
|
|
1765
|
+
|
|
1561
1766
|
var handleFormat = function handleFormat() {
|
|
1562
1767
|
if (!activeTab) return;
|
|
1563
1768
|
|
|
@@ -2789,6 +2994,13 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2789
2994
|
actions: React.createElement(
|
|
2790
2995
|
SectionActionGroup,
|
|
2791
2996
|
{ ariaLabel: "Project actions" },
|
|
2997
|
+
React.createElement(SidebarActionButton, {
|
|
2998
|
+
title: "Refresh workspace",
|
|
2999
|
+
iconClass: "fas fa-sync-alt",
|
|
3000
|
+
ariaBusy: !!loading.refreshWorkspace,
|
|
3001
|
+
onClick: handleRefreshWorkspace,
|
|
3002
|
+
disabled: !!loading.refreshWorkspace
|
|
3003
|
+
}),
|
|
2792
3004
|
React.createElement(SidebarActionButton, {
|
|
2793
3005
|
title: "Collapse all folders",
|
|
2794
3006
|
iconClass: "fas fa-compress-alt",
|
|
@@ -3775,6 +3987,12 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3775
3987
|
React.Fragment,
|
|
3776
3988
|
null,
|
|
3777
3989
|
renderTabBar(pane.id, pane.tabs, pane.activeTabId),
|
|
3990
|
+
React.createElement(FileReloadBanner, {
|
|
3991
|
+
pendingReloads: (state.pendingReloads || []).filter(function (r) { return r.paneId === pane.id; }),
|
|
3992
|
+
onSaveAndReload: handleSaveAndReload,
|
|
3993
|
+
onDiscardAndReload: handleDiscardAndReload,
|
|
3994
|
+
onKeepMine: handleKeepMine
|
|
3995
|
+
}),
|
|
3778
3996
|
React.createElement(
|
|
3779
3997
|
"div",
|
|
3780
3998
|
{ 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
|
+
})();
|
|
@@ -61,6 +61,7 @@ var SearchService = (function () {
|
|
|
61
61
|
|
|
62
62
|
function traverse(nodes) {
|
|
63
63
|
nodes.forEach(function(n) {
|
|
64
|
+
if (n.excluded) return; // skip excluded paths and their descendants
|
|
64
65
|
if (n.type === 'file') {
|
|
65
66
|
docs.push({ id: idCounter++, path: n.path, name: n.name, type: 'file' });
|
|
66
67
|
} else if (n.type === 'folder') {
|
|
@@ -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
|
+
}
|
|
@@ -702,7 +702,8 @@ module Mbeditor
|
|
|
702
702
|
|
|
703
703
|
# GET /mbeditor/manifest.webmanifest — PWA manifest
|
|
704
704
|
def pwa_manifest
|
|
705
|
-
|
|
705
|
+
_mount = request.script_name.to_s.gsub(%r{(^/+|/+$)}, "")
|
|
706
|
+
base = _mount.empty? ? "" : "/#{_mount}"
|
|
706
707
|
manifest = {
|
|
707
708
|
name: "Mbeditor — #{Rails.root.basename}",
|
|
708
709
|
short_name: "Mbeditor",
|
|
@@ -1070,10 +1071,14 @@ module Mbeditor
|
|
|
1070
1071
|
rel = relative_path(full)
|
|
1071
1072
|
|
|
1072
1073
|
if File.directory?(full)
|
|
1073
|
-
{ name: name, type: "folder", path: rel, children: build_tree(full, depth: depth + 1) }
|
|
1074
|
+
node = { name: name, type: "folder", path: rel, children: build_tree(full, depth: depth + 1) }
|
|
1075
|
+
node[:excluded] = true if excluded_path?(rel, name)
|
|
1076
|
+
node
|
|
1074
1077
|
else
|
|
1075
1078
|
size = File.size(full) rescue nil
|
|
1076
|
-
{ name: name, type: "file", path: rel, size: size }
|
|
1079
|
+
node = { name: name, type: "file", path: rel, size: size }
|
|
1080
|
+
node[:excluded] = true if excluded_path?(rel, name)
|
|
1081
|
+
node
|
|
1077
1082
|
end
|
|
1078
1083
|
end
|
|
1079
1084
|
rescue Errno::EACCES
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<meta name="theme-color" content="#1e1e2e" />
|
|
8
8
|
<meta name="mobile-web-app-capable" content="yes" />
|
|
9
9
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
10
|
-
<%
|
|
10
|
+
<% _mount = request.script_name.to_s.gsub(%r{(^/+|/+$)}, ''); pwa_base = _mount.empty? ? '' : "/#{_mount}" %>
|
|
11
11
|
<link rel="manifest" href="<%= "#{pwa_base}/manifest.webmanifest" %>" />
|
|
12
12
|
<script>
|
|
13
13
|
if ('serviceWorker' in navigator) {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
<script defer src="<%= asset_path('minisearch.min.js') %>"></script>
|
|
33
33
|
<script defer src="<%= asset_path('marked.min.js') %>"></script>
|
|
34
34
|
<!-- ── Monaco loader (sync — inline body script calls require()) ── -->
|
|
35
|
-
<% base_path =
|
|
35
|
+
<% base_path = pwa_base %>
|
|
36
36
|
<script>var require = { paths: { vs: '<%= json_escape("#{base_path}/monaco-editor/vs") %>', 'monaco-editor/esm/vs': '<%= json_escape("#{base_path}/monaco-editor/vs") %>', 'monaco-vim': '<%= json_escape(asset_path("monaco-vim.js").sub(/\.js$/, "")) %>' } };</script>
|
|
37
37
|
<script src="<%= "#{base_path}/monaco-editor/vs/loader.js" %>"></script>
|
|
38
38
|
<!-- ── Emmet + Extra themes (non-AMD, deferred — used inside Monaco callback) ── -->
|
data/lib/mbeditor/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mbeditor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oliver Noonan
|
|
@@ -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
|