@rooode/dsh-plugin-preview 0.1.10 → 0.1.12
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.
- package/README.md +6 -1
- package/lib/client.js +1107 -101
- package/lib/index.js +170 -2
- package/package.json +4 -3
package/lib/client.js
CHANGED
|
@@ -494,6 +494,97 @@
|
|
|
494
494
|
}
|
|
495
495
|
}
|
|
496
496
|
|
|
497
|
+
function renameOpenTabPaths(oldPath, newPath, isDir = false) {
|
|
498
|
+
if (!oldPath || !newPath) return;
|
|
499
|
+
let changed = false;
|
|
500
|
+
const oldNorm = oldPath.replace(/\\/g, '/').toLowerCase();
|
|
501
|
+
const newNorm = newPath.replace(/\\/g, '/');
|
|
502
|
+
|
|
503
|
+
globalTabs = globalTabs.map(tab => {
|
|
504
|
+
const tabNorm = tab.filePath.replace(/\\/g, '/').toLowerCase();
|
|
505
|
+
if (!isDir) {
|
|
506
|
+
if (tabNorm === oldNorm) {
|
|
507
|
+
changed = true;
|
|
508
|
+
const newFileName = getFileName(newPath);
|
|
509
|
+
const newExt = getFileExtension(newPath);
|
|
510
|
+
const newCat = getFileTypeCategory(newPath);
|
|
511
|
+
return {
|
|
512
|
+
...tab,
|
|
513
|
+
id: 'tab:' + newPath.toLowerCase(),
|
|
514
|
+
filePath: newPath,
|
|
515
|
+
title: newFileName,
|
|
516
|
+
extension: newExt,
|
|
517
|
+
category: newCat,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
} else {
|
|
521
|
+
if (tabNorm === oldNorm || tabNorm.startsWith(oldNorm + '/')) {
|
|
522
|
+
changed = true;
|
|
523
|
+
const subPath = tab.filePath.replace(/\\/g, '/').slice(oldNorm.length);
|
|
524
|
+
const updatedFilePath = newNorm + subPath;
|
|
525
|
+
const newFileName = getFileName(updatedFilePath);
|
|
526
|
+
const newExt = getFileExtension(updatedFilePath);
|
|
527
|
+
const newCat = getFileTypeCategory(updatedFilePath);
|
|
528
|
+
return {
|
|
529
|
+
...tab,
|
|
530
|
+
id: 'tab:' + updatedFilePath.toLowerCase(),
|
|
531
|
+
filePath: updatedFilePath,
|
|
532
|
+
title: newFileName,
|
|
533
|
+
extension: newExt,
|
|
534
|
+
category: newCat,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return tab;
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
if (changed) {
|
|
542
|
+
if (globalActiveTabId) {
|
|
543
|
+
const activeNorm = globalActiveTabId.toLowerCase();
|
|
544
|
+
const targetPrefix = 'tab:' + oldNorm;
|
|
545
|
+
if (activeNorm === targetPrefix) {
|
|
546
|
+
globalActiveTabId = 'tab:' + newPath.toLowerCase();
|
|
547
|
+
} else if (isDir && activeNorm.startsWith(targetPrefix + '/')) {
|
|
548
|
+
const sub = activeNorm.slice(targetPrefix.length);
|
|
549
|
+
globalActiveTabId = 'tab:' + newNorm.toLowerCase() + sub;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
saveTabsToStorage();
|
|
553
|
+
notifyStateChange();
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function removeOpenTabsForPath(targetPath, isDir = false) {
|
|
558
|
+
if (!targetPath) return;
|
|
559
|
+
let changed = false;
|
|
560
|
+
const delNorm = targetPath.replace(/\\/g, '/').toLowerCase();
|
|
561
|
+
const nextTabs = globalTabs.filter(tab => {
|
|
562
|
+
const tabNorm = tab.filePath.replace(/\\/g, '/').toLowerCase();
|
|
563
|
+
if (!isDir) {
|
|
564
|
+
if (tabNorm === delNorm) {
|
|
565
|
+
changed = true;
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
} else {
|
|
569
|
+
if (tabNorm === delNorm || tabNorm.startsWith(delNorm + '/')) {
|
|
570
|
+
changed = true;
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return true;
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
if (changed) {
|
|
578
|
+
globalTabs = nextTabs;
|
|
579
|
+
if (!globalTabs.some(t => t.id === globalActiveTabId)) {
|
|
580
|
+
globalActiveTabId = globalTabs.length > 0 ? globalTabs[0].id : null;
|
|
581
|
+
}
|
|
582
|
+
saveTabsToStorage();
|
|
583
|
+
notifyStateChange();
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
|
|
497
588
|
// =========================================================================
|
|
498
589
|
// Toast Notification Utility
|
|
499
590
|
// =========================================================================
|
|
@@ -949,12 +1040,398 @@
|
|
|
949
1040
|
}
|
|
950
1041
|
|
|
951
1042
|
// =========================================================================
|
|
952
|
-
// React Components: Workspace File Tree Explorer
|
|
1043
|
+
// React Components: Workspace File Tree Explorer (with DnD, Create & Move)
|
|
953
1044
|
// =========================================================================
|
|
954
|
-
|
|
1045
|
+
|
|
1046
|
+
function IconNewFile() {
|
|
1047
|
+
return h('svg', {
|
|
1048
|
+
width: 14,
|
|
1049
|
+
height: 14,
|
|
1050
|
+
viewBox: '0 0 16 16',
|
|
1051
|
+
fill: 'none',
|
|
1052
|
+
stroke: 'currentColor',
|
|
1053
|
+
strokeWidth: 1.3,
|
|
1054
|
+
strokeLinecap: 'round',
|
|
1055
|
+
strokeLinejoin: 'round',
|
|
1056
|
+
style: { display: 'block', flexShrink: 0 }
|
|
1057
|
+
},
|
|
1058
|
+
h('path', { d: 'M9 1.5H3.5A1.5 1.5 0 0 0 2 3v10a1.5 1.5 0 0 0 1.5 1.5h9a1.5 1.5 0 0 0 1.5-1.5V6L9 1.5z' }),
|
|
1059
|
+
h('polyline', { points: '9 1.5 9 6 13.5 6' }),
|
|
1060
|
+
h('line', { x1: 5.5, y1: 10, x2: 9.5, y2: 10 }),
|
|
1061
|
+
h('line', { x1: 7.5, y1: 8, x2: 7.5, y2: 12 })
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function IconNewFolder() {
|
|
1066
|
+
return h('svg', {
|
|
1067
|
+
width: 14,
|
|
1068
|
+
height: 14,
|
|
1069
|
+
viewBox: '0 0 16 16',
|
|
1070
|
+
fill: 'none',
|
|
1071
|
+
stroke: 'currentColor',
|
|
1072
|
+
strokeWidth: 1.3,
|
|
1073
|
+
strokeLinecap: 'round',
|
|
1074
|
+
strokeLinejoin: 'round',
|
|
1075
|
+
style: { display: 'block', flexShrink: 0 }
|
|
1076
|
+
},
|
|
1077
|
+
h('path', { d: 'M1.5 3.5A1.5 1.5 0 0 1 3 2h3.2l1.4 1.5H13A1.5 1.5 0 0 1 14.5 5v7A1.5 1.5 0 0 1 13 13.5H3A1.5 1.5 0 0 1 1.5 12V3.5z' }),
|
|
1078
|
+
h('line', { x1: 6, y1: 8.5, x2: 10, y2: 8.5 }),
|
|
1079
|
+
h('line', { x1: 8, y1: 6.5, x2: 8, y2: 10.5 })
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function IconRefresh() {
|
|
1084
|
+
return h('svg', {
|
|
1085
|
+
width: 13,
|
|
1086
|
+
height: 13,
|
|
1087
|
+
viewBox: '0 0 16 16',
|
|
1088
|
+
fill: 'none',
|
|
1089
|
+
stroke: 'currentColor',
|
|
1090
|
+
strokeWidth: 1.4,
|
|
1091
|
+
strokeLinecap: 'round',
|
|
1092
|
+
strokeLinejoin: 'round',
|
|
1093
|
+
style: { display: 'block', flexShrink: 0 }
|
|
1094
|
+
},
|
|
1095
|
+
h('path', { d: 'M13.5 2.5v3.5h-3.5' }),
|
|
1096
|
+
h('path', { d: 'M2.5 13.5v-3.5h3.5' }),
|
|
1097
|
+
h('path', { d: 'M3.5 6a5.5 5.5 0 0 1 8.8-1.5l1.2 1.5' }),
|
|
1098
|
+
h('path', { d: 'M12.5 10a5.5 5.5 0 0 1-8.8 1.5l-1.2-1.5' })
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function IconExpandAll() {
|
|
1103
|
+
return h('svg', {
|
|
1104
|
+
width: 13,
|
|
1105
|
+
height: 13,
|
|
1106
|
+
viewBox: '0 0 16 16',
|
|
1107
|
+
fill: 'none',
|
|
1108
|
+
stroke: 'currentColor',
|
|
1109
|
+
strokeWidth: 1.4,
|
|
1110
|
+
strokeLinecap: 'round',
|
|
1111
|
+
strokeLinejoin: 'round',
|
|
1112
|
+
style: { display: 'block', flexShrink: 0 }
|
|
1113
|
+
},
|
|
1114
|
+
h('polyline', { points: '3.5 5.5 8 2 12.5 5.5' }),
|
|
1115
|
+
h('line', { x1: 2, y1: 8, x2: 14, y2: 8 }),
|
|
1116
|
+
h('polyline', { points: '3.5 10.5 8 14 12.5 10.5' })
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function IconCollapseAll() {
|
|
1121
|
+
return h('svg', {
|
|
1122
|
+
width: 13,
|
|
1123
|
+
height: 13,
|
|
1124
|
+
viewBox: '0 0 16 16',
|
|
1125
|
+
fill: 'none',
|
|
1126
|
+
stroke: 'currentColor',
|
|
1127
|
+
strokeWidth: 1.4,
|
|
1128
|
+
strokeLinecap: 'round',
|
|
1129
|
+
strokeLinejoin: 'round',
|
|
1130
|
+
style: { display: 'block', flexShrink: 0 }
|
|
1131
|
+
},
|
|
1132
|
+
h('polyline', { points: '3.5 3 8 6.5 12.5 3' }),
|
|
1133
|
+
h('line', { x1: 2, y1: 8, x2: 14, y2: 8 }),
|
|
1134
|
+
h('polyline', { points: '3.5 13 8 9.5 12.5 13' })
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function IconChevronRight() {
|
|
1139
|
+
return h('svg', {
|
|
1140
|
+
width: 9,
|
|
1141
|
+
height: 9,
|
|
1142
|
+
viewBox: '0 0 16 16',
|
|
1143
|
+
fill: 'none',
|
|
1144
|
+
stroke: 'currentColor',
|
|
1145
|
+
strokeWidth: 2.2,
|
|
1146
|
+
strokeLinecap: 'round',
|
|
1147
|
+
strokeLinejoin: 'round',
|
|
1148
|
+
style: { display: 'block' }
|
|
1149
|
+
}, h('polyline', { points: '5 3 10 8 5 13' }));
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function IconChevronDown() {
|
|
1153
|
+
return h('svg', {
|
|
1154
|
+
width: 9,
|
|
1155
|
+
height: 9,
|
|
1156
|
+
viewBox: '0 0 16 16',
|
|
1157
|
+
fill: 'none',
|
|
1158
|
+
stroke: 'currentColor',
|
|
1159
|
+
strokeWidth: 2.2,
|
|
1160
|
+
strokeLinecap: 'round',
|
|
1161
|
+
strokeLinejoin: 'round',
|
|
1162
|
+
style: { display: 'block' }
|
|
1163
|
+
}, h('polyline', { points: '3 5 8 10 13 5' }));
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function IconClose() {
|
|
1167
|
+
return h('svg', {
|
|
1168
|
+
width: 12,
|
|
1169
|
+
height: 12,
|
|
1170
|
+
viewBox: '0 0 16 16',
|
|
1171
|
+
fill: 'none',
|
|
1172
|
+
stroke: 'currentColor',
|
|
1173
|
+
strokeWidth: 1.6,
|
|
1174
|
+
strokeLinecap: 'round',
|
|
1175
|
+
style: { display: 'block', flexShrink: 0 }
|
|
1176
|
+
},
|
|
1177
|
+
h('line', { x1: 3, y1: 3, x2: 13, y2: 13 }),
|
|
1178
|
+
h('line', { x1: 13, y1: 3, x2: 3, y2: 13 })
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function isValidDropTarget(draggedItem, targetNode) {
|
|
1183
|
+
if (!draggedItem || !targetNode) return false;
|
|
1184
|
+
if (!targetNode.isDir) return false;
|
|
1185
|
+
const src = draggedItem.path.replace(/\\/g, '/').toLowerCase();
|
|
1186
|
+
const dest = targetNode.path.replace(/\\/g, '/').toLowerCase();
|
|
1187
|
+
// Cannot drop on itself
|
|
1188
|
+
if (src === dest) return false;
|
|
1189
|
+
// Cannot drop into current parent directory (already there)
|
|
1190
|
+
const lastSlash = src.lastIndexOf('/');
|
|
1191
|
+
const srcParent = lastSlash > 0 ? src.substring(0, lastSlash) : '';
|
|
1192
|
+
if (srcParent && srcParent === dest) return false;
|
|
1193
|
+
// If dragged is a dir, cannot drop into its own children or itself
|
|
1194
|
+
if (draggedItem.isDir) {
|
|
1195
|
+
if (dest.startsWith(src + '/')) return false;
|
|
1196
|
+
}
|
|
1197
|
+
return true;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function InlineCreateInputNode({ depth = 0, type, onConfirm, onCancel }) {
|
|
1201
|
+
const [val, setVal] = useState('');
|
|
1202
|
+
const inputRef = useRef(null);
|
|
1203
|
+
|
|
1204
|
+
useEffect(() => {
|
|
1205
|
+
if (inputRef.current) {
|
|
1206
|
+
inputRef.current.focus();
|
|
1207
|
+
inputRef.current.select();
|
|
1208
|
+
}
|
|
1209
|
+
}, []);
|
|
1210
|
+
|
|
1211
|
+
const handleKeyDown = (e) => {
|
|
1212
|
+
if (e.key === 'Enter') {
|
|
1213
|
+
e.preventDefault();
|
|
1214
|
+
e.stopPropagation();
|
|
1215
|
+
if (val.trim()) onConfirm(val.trim());
|
|
1216
|
+
else onCancel();
|
|
1217
|
+
} else if (e.key === 'Escape') {
|
|
1218
|
+
e.preventDefault();
|
|
1219
|
+
e.stopPropagation();
|
|
1220
|
+
onCancel();
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
|
|
1224
|
+
const handleBlur = () => {
|
|
1225
|
+
if (!val.trim()) onCancel();
|
|
1226
|
+
};
|
|
1227
|
+
|
|
1228
|
+
return h('div', {
|
|
1229
|
+
className: 'dsh-tree-inline-input-row',
|
|
1230
|
+
style: { paddingLeft: `${depth * 14 + 6}px` },
|
|
1231
|
+
onClick: (e) => e.stopPropagation(),
|
|
1232
|
+
},
|
|
1233
|
+
h('span', { className: 'dsh-tree-chevron-placeholder' }),
|
|
1234
|
+
h('span', {
|
|
1235
|
+
className: 'dsh-tree-icon',
|
|
1236
|
+
style: { color: type === 'dir' ? '#eab308' : '#38bdf8' },
|
|
1237
|
+
}, type === 'dir' ? '📁' : '📄'),
|
|
1238
|
+
h('input', {
|
|
1239
|
+
ref: inputRef,
|
|
1240
|
+
type: 'text',
|
|
1241
|
+
className: 'dsh-tree-inline-input',
|
|
1242
|
+
value: val,
|
|
1243
|
+
placeholder: type === 'dir' ? '新文件夹名称...' : '新文件名 (如 doc.md)...',
|
|
1244
|
+
onChange: (e) => setVal(e.target.value),
|
|
1245
|
+
onKeyDown: handleKeyDown,
|
|
1246
|
+
onBlur: handleBlur,
|
|
1247
|
+
})
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function InlineRenameInputNode({ node, depth = 0, onConfirm, onCancel }) {
|
|
1252
|
+
const [val, setVal] = useState(node.name);
|
|
1253
|
+
const inputRef = useRef(null);
|
|
1254
|
+
|
|
1255
|
+
useEffect(() => {
|
|
1256
|
+
if (inputRef.current) {
|
|
1257
|
+
inputRef.current.focus();
|
|
1258
|
+
if (!node.isDir && node.name.includes('.')) {
|
|
1259
|
+
const lastDot = node.name.lastIndexOf('.');
|
|
1260
|
+
if (lastDot > 0) {
|
|
1261
|
+
inputRef.current.setSelectionRange(0, lastDot);
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
inputRef.current.select();
|
|
1266
|
+
}
|
|
1267
|
+
}, [node]);
|
|
1268
|
+
|
|
1269
|
+
const handleKeyDown = (e) => {
|
|
1270
|
+
if (e.key === 'Enter') {
|
|
1271
|
+
e.preventDefault();
|
|
1272
|
+
e.stopPropagation();
|
|
1273
|
+
if (val.trim() && val.trim() !== node.name) {
|
|
1274
|
+
onConfirm(val.trim());
|
|
1275
|
+
} else {
|
|
1276
|
+
onCancel();
|
|
1277
|
+
}
|
|
1278
|
+
} else if (e.key === 'Escape') {
|
|
1279
|
+
e.preventDefault();
|
|
1280
|
+
e.stopPropagation();
|
|
1281
|
+
onCancel();
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
|
|
1285
|
+
const handleBlur = () => {
|
|
1286
|
+
onCancel();
|
|
1287
|
+
};
|
|
1288
|
+
|
|
1289
|
+
const iconInfo = getFileIcon(node);
|
|
1290
|
+
|
|
1291
|
+
return h('div', {
|
|
1292
|
+
className: 'dsh-tree-inline-input-row',
|
|
1293
|
+
style: { paddingLeft: `${depth * 14 + 6}px` },
|
|
1294
|
+
onClick: (e) => e.stopPropagation(),
|
|
1295
|
+
},
|
|
1296
|
+
h('span', { className: 'dsh-tree-chevron-placeholder' }),
|
|
1297
|
+
h('span', {
|
|
1298
|
+
className: 'dsh-tree-icon',
|
|
1299
|
+
style: { color: node.isDir ? '#eab308' : iconInfo.color },
|
|
1300
|
+
}, node.isDir ? '📁' : iconInfo.icon),
|
|
1301
|
+
h('input', {
|
|
1302
|
+
ref: inputRef,
|
|
1303
|
+
type: 'text',
|
|
1304
|
+
className: 'dsh-tree-inline-input',
|
|
1305
|
+
value: val,
|
|
1306
|
+
onChange: (e) => setVal(e.target.value),
|
|
1307
|
+
onKeyDown: handleKeyDown,
|
|
1308
|
+
onBlur: handleBlur,
|
|
1309
|
+
})
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
function TreeContextMenu({ x, y, node, isRoot, clipboard, onAction, onClose }) {
|
|
1314
|
+
const menuRef = useRef(null);
|
|
1315
|
+
useEffect(() => {
|
|
1316
|
+
const handleDown = (e) => {
|
|
1317
|
+
if (menuRef.current && !menuRef.current.contains(e.target)) {
|
|
1318
|
+
onClose();
|
|
1319
|
+
}
|
|
1320
|
+
};
|
|
1321
|
+
window.addEventListener('mousedown', handleDown, true);
|
|
1322
|
+
window.addEventListener('scroll', onClose, true);
|
|
1323
|
+
return () => {
|
|
1324
|
+
window.removeEventListener('mousedown', handleDown, true);
|
|
1325
|
+
window.removeEventListener('scroll', onClose, true);
|
|
1326
|
+
};
|
|
1327
|
+
}, [onClose]);
|
|
1328
|
+
|
|
1329
|
+
const style = {
|
|
1330
|
+
position: 'fixed',
|
|
1331
|
+
top: `${Math.min(Math.max(10, y), window.innerHeight - 300)}px`,
|
|
1332
|
+
left: `${Math.min(Math.max(10, x), window.innerWidth - 210)}px`,
|
|
1333
|
+
zIndex: 99999,
|
|
1334
|
+
};
|
|
1335
|
+
|
|
1336
|
+
const isDir = isRoot || (node && node.isDir);
|
|
1337
|
+
|
|
1338
|
+
return h('div', { ref: menuRef, className: 'dsh-tree-context-menu', style },
|
|
1339
|
+
isDir ? [
|
|
1340
|
+
h('div', { key: 'new-file', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('new-file', node); } },
|
|
1341
|
+
h('span', { className: 'dsh-menu-icon' }, '📄'),
|
|
1342
|
+
h('span', null, '新建文件')
|
|
1343
|
+
),
|
|
1344
|
+
h('div', { key: 'new-dir', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('new-dir', node); } },
|
|
1345
|
+
h('span', { className: 'dsh-menu-icon' }, '📁'),
|
|
1346
|
+
h('span', null, '新建文件夹')
|
|
1347
|
+
),
|
|
1348
|
+
h('div', { key: 'div-1', className: 'dsh-menu-divider' }),
|
|
1349
|
+
] : null,
|
|
1350
|
+
|
|
1351
|
+
!isRoot && !isDir ? h('div', { key: 'preview', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('preview', node); } },
|
|
1352
|
+
h('span', { className: 'dsh-menu-icon' }, '📖'),
|
|
1353
|
+
h('span', null, '打开预览')
|
|
1354
|
+
) : null,
|
|
1355
|
+
|
|
1356
|
+
!isRoot ? h('div', { key: 'cut', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('cut', node); } },
|
|
1357
|
+
h('span', { className: 'dsh-menu-icon' }, '✂️'),
|
|
1358
|
+
h('span', null, '剪切')
|
|
1359
|
+
) : null,
|
|
1360
|
+
|
|
1361
|
+
isDir && clipboard && clipboard.action === 'cut' ? h('div', {
|
|
1362
|
+
key: 'paste',
|
|
1363
|
+
className: 'dsh-menu-item',
|
|
1364
|
+
onClick: () => { onClose(); onAction('paste', node); }
|
|
1365
|
+
},
|
|
1366
|
+
h('span', { className: 'dsh-menu-icon' }, '📋'),
|
|
1367
|
+
h('span', null, `粘贴 "${clipboard.name}" 到此处`)
|
|
1368
|
+
) : null,
|
|
1369
|
+
|
|
1370
|
+
!isRoot ? h('div', { key: 'rename', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('rename', node); } },
|
|
1371
|
+
h('span', { className: 'dsh-menu-icon' }, '🏷️'),
|
|
1372
|
+
h('span', null, '重命名')
|
|
1373
|
+
) : null,
|
|
1374
|
+
|
|
1375
|
+
!isRoot ? h('div', { key: 'div-2', className: 'dsh-menu-divider' }) : null,
|
|
1376
|
+
|
|
1377
|
+
!isRoot ? h('div', { key: 'reveal', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('reveal', node); } },
|
|
1378
|
+
h('span', { className: 'dsh-menu-icon' }, '📂'),
|
|
1379
|
+
h('span', null, '在文件管理器中定位')
|
|
1380
|
+
) : null,
|
|
1381
|
+
|
|
1382
|
+
!isRoot && !isDir ? h('div', { key: 'native', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('native', node); } },
|
|
1383
|
+
h('span', { className: 'dsh-menu-icon' }, '🌐'),
|
|
1384
|
+
h('span', null, '系统默认应用打开')
|
|
1385
|
+
) : null,
|
|
1386
|
+
|
|
1387
|
+
!isRoot ? [
|
|
1388
|
+
h('div', { key: 'div-3', className: 'dsh-menu-divider' }),
|
|
1389
|
+
h('div', { key: 'delete', className: 'dsh-menu-item dsh-menu-item-danger', onClick: () => { onClose(); onAction('delete', node); } },
|
|
1390
|
+
h('span', { className: 'dsh-menu-icon' }, '🗑️'),
|
|
1391
|
+
h('span', null, '删除')
|
|
1392
|
+
)
|
|
1393
|
+
] : null,
|
|
1394
|
+
|
|
1395
|
+
isRoot ? [
|
|
1396
|
+
h('div', { key: 'refresh', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('refresh'); } },
|
|
1397
|
+
h('span', { className: 'dsh-menu-icon' }, '🔄'),
|
|
1398
|
+
h('span', null, '刷新目录树')
|
|
1399
|
+
)
|
|
1400
|
+
] : null
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
function FileTreeNodeItem({
|
|
1405
|
+
node,
|
|
1406
|
+
depth = 0,
|
|
1407
|
+
expandedPaths,
|
|
1408
|
+
onToggleExpand,
|
|
1409
|
+
activeFilePath,
|
|
1410
|
+
onOpenFile,
|
|
1411
|
+
searchQuery,
|
|
1412
|
+
creatingItem,
|
|
1413
|
+
onConfirmCreate,
|
|
1414
|
+
onCancelCreate,
|
|
1415
|
+
renamingPath,
|
|
1416
|
+
onConfirmRename,
|
|
1417
|
+
onCancelRename,
|
|
1418
|
+
onContextMenu,
|
|
1419
|
+
draggedNode,
|
|
1420
|
+
dragOverNode,
|
|
1421
|
+
onDragStart,
|
|
1422
|
+
onDragEnd,
|
|
1423
|
+
onDragOver,
|
|
1424
|
+
onDragLeave,
|
|
1425
|
+
onDrop,
|
|
1426
|
+
clipboard,
|
|
1427
|
+
}) {
|
|
955
1428
|
const isDir = node.isDir;
|
|
956
1429
|
const isExpanded = expandedPaths.has(node.path);
|
|
957
1430
|
const isActive = !isDir && activeFilePath && activeFilePath.toLowerCase() === node.path.toLowerCase();
|
|
1431
|
+
const isRenaming = renamingPath && renamingPath.toLowerCase() === node.path.toLowerCase();
|
|
1432
|
+
const isDragged = draggedNode && draggedNode.path && draggedNode.path.toLowerCase() === node.path.toLowerCase();
|
|
1433
|
+
const isDropTarget = isDir && dragOverNode && dragOverNode.path && dragOverNode.path.toLowerCase() === node.path.toLowerCase();
|
|
1434
|
+
const isCut = clipboard && clipboard.action === 'cut' && clipboard.path && clipboard.path.toLowerCase() === node.path.toLowerCase();
|
|
958
1435
|
const iconInfo = getFileIcon(node);
|
|
959
1436
|
|
|
960
1437
|
const indentStyle = { paddingLeft: `${depth * 14 + 6}px` };
|
|
@@ -968,17 +1445,71 @@
|
|
|
968
1445
|
}
|
|
969
1446
|
};
|
|
970
1447
|
|
|
1448
|
+
const handleContextMenu = (e) => {
|
|
1449
|
+
e.preventDefault();
|
|
1450
|
+
e.stopPropagation();
|
|
1451
|
+
onContextMenu(node, e);
|
|
1452
|
+
};
|
|
1453
|
+
|
|
1454
|
+
const handleDragStart = (e) => {
|
|
1455
|
+
e.stopPropagation();
|
|
1456
|
+
e.dataTransfer.setData('text/plain', node.path);
|
|
1457
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
1458
|
+
onDragStart(node);
|
|
1459
|
+
};
|
|
1460
|
+
|
|
1461
|
+
const handleDragOver = (e) => {
|
|
1462
|
+
if (isDir && isValidDropTarget(draggedNode, node)) {
|
|
1463
|
+
e.preventDefault();
|
|
1464
|
+
e.stopPropagation();
|
|
1465
|
+
e.dataTransfer.dropEffect = 'move';
|
|
1466
|
+
onDragOver(node);
|
|
1467
|
+
}
|
|
1468
|
+
};
|
|
1469
|
+
|
|
1470
|
+
const handleDragLeave = (e) => {
|
|
1471
|
+
if (isDir) {
|
|
1472
|
+
e.stopPropagation();
|
|
1473
|
+
onDragLeave(node);
|
|
1474
|
+
}
|
|
1475
|
+
};
|
|
1476
|
+
|
|
1477
|
+
const handleDrop = (e) => {
|
|
1478
|
+
if (isDir && isValidDropTarget(draggedNode, node)) {
|
|
1479
|
+
e.preventDefault();
|
|
1480
|
+
e.stopPropagation();
|
|
1481
|
+
onDrop(draggedNode, node);
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
|
|
1485
|
+
if (isRenaming) {
|
|
1486
|
+
return h(InlineRenameInputNode, {
|
|
1487
|
+
node,
|
|
1488
|
+
depth,
|
|
1489
|
+
onConfirm: (newName) => onConfirmRename(node, newName),
|
|
1490
|
+
onCancel: onCancelRename,
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
|
|
971
1494
|
return h(Fragment, null,
|
|
972
1495
|
h('div', {
|
|
973
|
-
className: `dsh-tree-node-row ${isDir ? 'is-dir' : 'is-file'} ${isActive ? 'active' : ''}`,
|
|
1496
|
+
className: `dsh-tree-node-row ${isDir ? 'is-dir' : 'is-file'} ${isActive ? 'active' : ''} ${isDropTarget ? 'drop-target' : ''} ${isDragged ? 'is-dragged' : ''} ${isCut ? 'is-cut' : ''}`,
|
|
974
1497
|
style: indentStyle,
|
|
975
1498
|
onClick: handleClick,
|
|
1499
|
+
onContextMenu: handleContextMenu,
|
|
1500
|
+
draggable: true,
|
|
1501
|
+
onDragStart: handleDragStart,
|
|
1502
|
+
onDragEnd: onDragEnd,
|
|
1503
|
+
onDragOver: handleDragOver,
|
|
1504
|
+
onDragLeave: handleDragLeave,
|
|
1505
|
+
onDrop: handleDrop,
|
|
976
1506
|
title: `${node.name}\n${node.path}${node.size ? ' (' + formatBytes(node.size) + ')' : ''}`,
|
|
977
1507
|
},
|
|
978
1508
|
// Chevron indicator for directory
|
|
979
1509
|
isDir ? h('span', {
|
|
980
1510
|
className: `dsh-tree-chevron ${isExpanded ? 'expanded' : ''}`,
|
|
981
|
-
|
|
1511
|
+
onClick: (e) => { e.stopPropagation(); onToggleExpand(node.path); },
|
|
1512
|
+
}, isExpanded ? h(IconChevronDown) : h(IconChevronRight)) : h('span', { className: 'dsh-tree-chevron-placeholder' }),
|
|
982
1513
|
|
|
983
1514
|
// File / Directory Icon
|
|
984
1515
|
h('span', {
|
|
@@ -995,18 +1526,47 @@
|
|
|
995
1526
|
)
|
|
996
1527
|
),
|
|
997
1528
|
|
|
998
|
-
// Children if expanded
|
|
999
|
-
isDir && isExpanded
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
node
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1529
|
+
// Children or Inline creation node if expanded
|
|
1530
|
+
isDir && isExpanded ? (
|
|
1531
|
+
h(Fragment, null,
|
|
1532
|
+
// If creating inside this directory, render inline creation node first
|
|
1533
|
+
creatingItem && creatingItem.parentPath && creatingItem.parentPath.replace(/\\/g, '/').toLowerCase() === node.path.replace(/\\/g, '/').toLowerCase() ? (
|
|
1534
|
+
h(InlineCreateInputNode, {
|
|
1535
|
+
depth: depth + 1,
|
|
1536
|
+
type: creatingItem.type,
|
|
1537
|
+
onConfirm: onConfirmCreate,
|
|
1538
|
+
onCancel: onCancelCreate,
|
|
1539
|
+
})
|
|
1540
|
+
) : null,
|
|
1541
|
+
|
|
1542
|
+
node.children && node.children.length > 0 ? (
|
|
1543
|
+
node.children.map(child => h(FileTreeNodeItem, {
|
|
1544
|
+
key: child.path,
|
|
1545
|
+
node: child,
|
|
1546
|
+
depth: depth + 1,
|
|
1547
|
+
expandedPaths,
|
|
1548
|
+
onToggleExpand,
|
|
1549
|
+
activeFilePath,
|
|
1550
|
+
onOpenFile,
|
|
1551
|
+
searchQuery,
|
|
1552
|
+
creatingItem,
|
|
1553
|
+
onConfirmCreate,
|
|
1554
|
+
onCancelCreate,
|
|
1555
|
+
renamingPath,
|
|
1556
|
+
onConfirmRename,
|
|
1557
|
+
onCancelRename,
|
|
1558
|
+
onContextMenu,
|
|
1559
|
+
draggedNode,
|
|
1560
|
+
dragOverNode,
|
|
1561
|
+
onDragStart,
|
|
1562
|
+
onDragEnd,
|
|
1563
|
+
onDragOver,
|
|
1564
|
+
onDragLeave,
|
|
1565
|
+
onDrop,
|
|
1566
|
+
clipboard,
|
|
1567
|
+
}))
|
|
1568
|
+
) : null
|
|
1569
|
+
)
|
|
1010
1570
|
) : null
|
|
1011
1571
|
);
|
|
1012
1572
|
}
|
|
@@ -1060,6 +1620,15 @@
|
|
|
1060
1620
|
const [isSelectorOpen, setIsSelectorOpen] = useState(false);
|
|
1061
1621
|
const [customPathInput, setCustomPathInput] = useState('');
|
|
1062
1622
|
const [validating, setValidating] = useState(false);
|
|
1623
|
+
|
|
1624
|
+
// File operations state
|
|
1625
|
+
const [creatingItem, setCreatingItem] = useState(null); // { parentPath: string, type: 'dir' | 'file' } | null
|
|
1626
|
+
const [renamingPath, setRenamingPath] = useState(null); // string | null
|
|
1627
|
+
const [contextMenu, setContextMenu] = useState(null); // { x: number, y: number, node: any, isRoot: boolean } | null
|
|
1628
|
+
const [draggedNode, setDraggedNode] = useState(null);
|
|
1629
|
+
const [dragOverNode, setDragOverNode] = useState(null);
|
|
1630
|
+
const [clipboard, setClipboard] = useState(null); // { action: 'cut', path: string, name: string, isDir: boolean } | null
|
|
1631
|
+
|
|
1063
1632
|
const currentWs = getCurrentWorkspaceRoot();
|
|
1064
1633
|
|
|
1065
1634
|
const loadTree = useCallback(async (force = false) => {
|
|
@@ -1121,6 +1690,17 @@
|
|
|
1121
1690
|
}
|
|
1122
1691
|
}, [searchQuery, treeData]);
|
|
1123
1692
|
|
|
1693
|
+
// Listen to external workspace switch events (e.g. from Git plugin)
|
|
1694
|
+
useEffect(() => {
|
|
1695
|
+
const handleWsSwitch = (e) => {
|
|
1696
|
+
if (e.detail?.path && e.detail.path !== globalManualWorkspace) {
|
|
1697
|
+
setManualWorkspace(e.detail.path);
|
|
1698
|
+
}
|
|
1699
|
+
};
|
|
1700
|
+
window.addEventListener('dsh:workspace:switch', handleWsSwitch);
|
|
1701
|
+
return () => window.removeEventListener('dsh:workspace:switch', handleWsSwitch);
|
|
1702
|
+
}, []);
|
|
1703
|
+
|
|
1124
1704
|
const availableWorkspaces = useMemo(() => {
|
|
1125
1705
|
return getAllAvailableWorkspaces();
|
|
1126
1706
|
}, [isSelectorOpen, globalManualWorkspace]);
|
|
@@ -1129,6 +1709,9 @@
|
|
|
1129
1709
|
setManualWorkspace(wsPath);
|
|
1130
1710
|
setIsSelectorOpen(false);
|
|
1131
1711
|
showToast(`已切换工作区: ${getFileName(wsPath)}`, 'success');
|
|
1712
|
+
if (typeof window !== 'undefined') {
|
|
1713
|
+
window.dispatchEvent(new CustomEvent('dsh:workspace:switch', { detail: { path: wsPath } }));
|
|
1714
|
+
}
|
|
1132
1715
|
};
|
|
1133
1716
|
|
|
1134
1717
|
const handleCustomSubmit = async (e) => {
|
|
@@ -1148,6 +1731,9 @@
|
|
|
1148
1731
|
setCustomPathInput('');
|
|
1149
1732
|
setIsSelectorOpen(false);
|
|
1150
1733
|
showToast(`已切换工作空间: ${json.name}`, 'success');
|
|
1734
|
+
if (typeof window !== 'undefined') {
|
|
1735
|
+
window.dispatchEvent(new CustomEvent('dsh:workspace:switch', { detail: { path: json.path } }));
|
|
1736
|
+
}
|
|
1151
1737
|
} else {
|
|
1152
1738
|
showToast(json?.error?.message || '指定目录不存在或不是文件夹', 'error');
|
|
1153
1739
|
}
|
|
@@ -1164,6 +1750,168 @@
|
|
|
1164
1750
|
showToast('已恢复根据当前会话自动识别工作空间', 'info');
|
|
1165
1751
|
};
|
|
1166
1752
|
|
|
1753
|
+
// =========================================================================
|
|
1754
|
+
// File Operations Handlers
|
|
1755
|
+
// =========================================================================
|
|
1756
|
+
const handleStartCreate = (parentPath, type) => {
|
|
1757
|
+
const root = treeData?.workspaceRoot || currentWs;
|
|
1758
|
+
const target = parentPath || root;
|
|
1759
|
+
setCreatingItem({ parentPath: target, type });
|
|
1760
|
+
if (target) {
|
|
1761
|
+
setExpandedPaths(prev => new Set(prev).add(target));
|
|
1762
|
+
}
|
|
1763
|
+
};
|
|
1764
|
+
|
|
1765
|
+
const handleConfirmCreate = async (name) => {
|
|
1766
|
+
if (!creatingItem || !name) return;
|
|
1767
|
+
const isDir = creatingItem.type === 'dir';
|
|
1768
|
+
const endpoint = isDir ? '/api/preview/create-dir' : '/api/preview/create-file';
|
|
1769
|
+
try {
|
|
1770
|
+
const res = await fetch(endpoint, {
|
|
1771
|
+
method: 'POST',
|
|
1772
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1773
|
+
body: JSON.stringify({
|
|
1774
|
+
parentPath: creatingItem.parentPath,
|
|
1775
|
+
name: name.trim(),
|
|
1776
|
+
}),
|
|
1777
|
+
});
|
|
1778
|
+
const json = await res.json();
|
|
1779
|
+
if (json.ok) {
|
|
1780
|
+
setCreatingItem(null);
|
|
1781
|
+
setExpandedPaths(prev => new Set(prev).add(creatingItem.parentPath));
|
|
1782
|
+
await loadTree(true);
|
|
1783
|
+
showToast(`已创建${isDir ? '文件夹' : '文件'}: ${json.name}`, 'success');
|
|
1784
|
+
if (!isDir && json.path) {
|
|
1785
|
+
openPreviewFile(json.path);
|
|
1786
|
+
}
|
|
1787
|
+
} else {
|
|
1788
|
+
showToast(json?.error?.message || `创建${isDir ? '文件夹' : '文件'}失败`, 'error');
|
|
1789
|
+
}
|
|
1790
|
+
} catch (err) {
|
|
1791
|
+
showToast(`创建失败: ${err.message}`, 'error');
|
|
1792
|
+
}
|
|
1793
|
+
};
|
|
1794
|
+
|
|
1795
|
+
const handleConfirmRename = async (node, newName) => {
|
|
1796
|
+
if (!node || !newName || newName.trim() === node.name) {
|
|
1797
|
+
setRenamingPath(null);
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
try {
|
|
1801
|
+
const res = await fetch('/api/preview/rename', {
|
|
1802
|
+
method: 'POST',
|
|
1803
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1804
|
+
body: JSON.stringify({
|
|
1805
|
+
path: node.path,
|
|
1806
|
+
newName: newName.trim(),
|
|
1807
|
+
}),
|
|
1808
|
+
});
|
|
1809
|
+
const json = await res.json();
|
|
1810
|
+
if (json.ok) {
|
|
1811
|
+
setRenamingPath(null);
|
|
1812
|
+
renameOpenTabPaths(json.sourcePath, json.targetPath, node.isDir);
|
|
1813
|
+
await loadTree(true);
|
|
1814
|
+
showToast(`已重命名为: ${json.name}`, 'success');
|
|
1815
|
+
} else {
|
|
1816
|
+
showToast(json?.error?.message || '重命名失败', 'error');
|
|
1817
|
+
}
|
|
1818
|
+
} catch (err) {
|
|
1819
|
+
showToast(`重命名失败: ${err.message}`, 'error');
|
|
1820
|
+
}
|
|
1821
|
+
};
|
|
1822
|
+
|
|
1823
|
+
const handleMove = async (sourcePath, targetDirPath) => {
|
|
1824
|
+
if (!sourcePath || !targetDirPath) return;
|
|
1825
|
+
try {
|
|
1826
|
+
const res = await fetch('/api/preview/move', {
|
|
1827
|
+
method: 'POST',
|
|
1828
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1829
|
+
body: JSON.stringify({
|
|
1830
|
+
sourcePath,
|
|
1831
|
+
targetPath: targetDirPath,
|
|
1832
|
+
}),
|
|
1833
|
+
});
|
|
1834
|
+
const json = await res.json();
|
|
1835
|
+
if (json.ok) {
|
|
1836
|
+
if (json.noop) {
|
|
1837
|
+
showToast('目标路径与源路径相同', 'info');
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
renameOpenTabPaths(json.sourcePath, json.targetPath, json.isDir);
|
|
1841
|
+
setExpandedPaths(prev => new Set(prev).add(targetDirPath));
|
|
1842
|
+
await loadTree(true);
|
|
1843
|
+
showToast(`已移动 ${json.name} 到 ${getFileName(targetDirPath)}`, 'success');
|
|
1844
|
+
} else {
|
|
1845
|
+
showToast(json?.error?.message || '移动文件失败', 'error');
|
|
1846
|
+
}
|
|
1847
|
+
} catch (err) {
|
|
1848
|
+
showToast(`移动失败: ${err.message}`, 'error');
|
|
1849
|
+
}
|
|
1850
|
+
};
|
|
1851
|
+
|
|
1852
|
+
const handleDelete = async (node) => {
|
|
1853
|
+
if (!node) return;
|
|
1854
|
+
const isDir = node.isDir;
|
|
1855
|
+
const msg = `确定要永久删除${isDir ? '文件夹' : '文件'} "${node.name}" 吗?此操作不可恢复。`;
|
|
1856
|
+
if (typeof window !== 'undefined' && !window.confirm(msg)) return;
|
|
1857
|
+
|
|
1858
|
+
try {
|
|
1859
|
+
const res = await fetch('/api/preview/delete', {
|
|
1860
|
+
method: 'POST',
|
|
1861
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1862
|
+
body: JSON.stringify({ path: node.path }),
|
|
1863
|
+
});
|
|
1864
|
+
const json = await res.json();
|
|
1865
|
+
if (json.ok) {
|
|
1866
|
+
removeOpenTabsForPath(node.path, isDir);
|
|
1867
|
+
await loadTree(true);
|
|
1868
|
+
showToast(`已删除${isDir ? '文件夹' : '文件'}: ${node.name}`, 'info');
|
|
1869
|
+
} else {
|
|
1870
|
+
showToast(json?.error?.message || '删除失败', 'error');
|
|
1871
|
+
}
|
|
1872
|
+
} catch (err) {
|
|
1873
|
+
showToast(`删除失败: ${err.message}`, 'error');
|
|
1874
|
+
}
|
|
1875
|
+
};
|
|
1876
|
+
|
|
1877
|
+
const handleContextMenuAction = (action, node) => {
|
|
1878
|
+
const rootPath = treeData?.workspaceRoot || currentWs;
|
|
1879
|
+
if (action === 'new-file') {
|
|
1880
|
+
const target = node ? (node.isDir ? node.path : (node.path.substring(0, Math.max(node.path.lastIndexOf('/'), node.path.lastIndexOf('\\'))))) : rootPath;
|
|
1881
|
+
handleStartCreate(target, 'file');
|
|
1882
|
+
} else if (action === 'new-dir') {
|
|
1883
|
+
const target = node ? (node.isDir ? node.path : (node.path.substring(0, Math.max(node.path.lastIndexOf('/'), node.path.lastIndexOf('\\'))))) : rootPath;
|
|
1884
|
+
handleStartCreate(target, 'dir');
|
|
1885
|
+
} else if (action === 'preview') {
|
|
1886
|
+
if (node && !node.isDir) onOpenFile(node.path);
|
|
1887
|
+
} else if (action === 'cut') {
|
|
1888
|
+
if (node) {
|
|
1889
|
+
setClipboard({ action: 'cut', path: node.path, name: node.name, isDir: node.isDir });
|
|
1890
|
+
showToast(`已剪切: ${node.name}`, 'info');
|
|
1891
|
+
}
|
|
1892
|
+
} else if (action === 'paste') {
|
|
1893
|
+
if (clipboard && clipboard.action === 'cut') {
|
|
1894
|
+
const target = node ? (node.isDir ? node.path : (node.path.substring(0, Math.max(node.path.lastIndexOf('/'), node.path.lastIndexOf('\\'))))) : rootPath;
|
|
1895
|
+
handleMove(clipboard.path, target);
|
|
1896
|
+
setClipboard(null);
|
|
1897
|
+
}
|
|
1898
|
+
} else if (action === 'rename') {
|
|
1899
|
+
if (node) setRenamingPath(node.path);
|
|
1900
|
+
} else if (action === 'reveal') {
|
|
1901
|
+
if (node) triggerRevealInExplorer(node.path);
|
|
1902
|
+
} else if (action === 'native') {
|
|
1903
|
+
if (node) triggerOpenNative(node.path);
|
|
1904
|
+
} else if (action === 'delete') {
|
|
1905
|
+
if (node) handleDelete(node);
|
|
1906
|
+
} else if (action === 'refresh') {
|
|
1907
|
+
loadTree(true);
|
|
1908
|
+
}
|
|
1909
|
+
};
|
|
1910
|
+
|
|
1911
|
+
const rootPath = treeData?.workspaceRoot || currentWs;
|
|
1912
|
+
const isCreatingAtRoot = creatingItem && creatingItem.parentPath && rootPath && creatingItem.parentPath.replace(/\\/g, '/').toLowerCase() === rootPath.replace(/\\/g, '/').toLowerCase();
|
|
1913
|
+
const isRootDropTarget = dragOverNode && rootPath && dragOverNode.path && dragOverNode.path.replace(/\\/g, '/').toLowerCase() === rootPath.replace(/\\/g, '/').toLowerCase();
|
|
1914
|
+
|
|
1167
1915
|
return h('div', {
|
|
1168
1916
|
className: 'dsh-tree-sidebar',
|
|
1169
1917
|
style: { width: `${width}px` },
|
|
@@ -1180,30 +1928,42 @@
|
|
|
1180
1928
|
h('span', { className: 'dsh-tree-header-chevron' }, isSelectorOpen ? '▲' : '▼')
|
|
1181
1929
|
),
|
|
1182
1930
|
h('div', { className: 'dsh-tree-header-actions' },
|
|
1931
|
+
h('button', {
|
|
1932
|
+
type: 'button',
|
|
1933
|
+
className: 'dsh-tree-action-btn',
|
|
1934
|
+
onClick: () => handleStartCreate(rootPath, 'file'),
|
|
1935
|
+
title: '新建文件 (在根目录)',
|
|
1936
|
+
}, h(IconNewFile)),
|
|
1937
|
+
h('button', {
|
|
1938
|
+
type: 'button',
|
|
1939
|
+
className: 'dsh-tree-action-btn',
|
|
1940
|
+
onClick: () => handleStartCreate(rootPath, 'dir'),
|
|
1941
|
+
title: '新建文件夹 (在根目录)',
|
|
1942
|
+
}, h(IconNewFolder)),
|
|
1183
1943
|
h('button', {
|
|
1184
1944
|
type: 'button',
|
|
1185
1945
|
className: 'dsh-tree-action-btn',
|
|
1186
1946
|
onClick: () => loadTree(true),
|
|
1187
1947
|
title: '刷新文件树',
|
|
1188
|
-
},
|
|
1948
|
+
}, h(IconRefresh)),
|
|
1189
1949
|
h('button', {
|
|
1190
1950
|
type: 'button',
|
|
1191
1951
|
className: 'dsh-tree-action-btn',
|
|
1192
1952
|
onClick: handleExpandAll,
|
|
1193
1953
|
title: '全部展开',
|
|
1194
|
-
},
|
|
1954
|
+
}, h(IconExpandAll)),
|
|
1195
1955
|
h('button', {
|
|
1196
1956
|
type: 'button',
|
|
1197
1957
|
className: 'dsh-tree-action-btn',
|
|
1198
1958
|
onClick: handleCollapseAll,
|
|
1199
1959
|
title: '全部折叠',
|
|
1200
|
-
},
|
|
1960
|
+
}, h(IconCollapseAll)),
|
|
1201
1961
|
onCloseSidebar ? h('button', {
|
|
1202
1962
|
type: 'button',
|
|
1203
1963
|
className: 'dsh-tree-action-btn',
|
|
1204
1964
|
onClick: onCloseSidebar,
|
|
1205
1965
|
title: '收起目录树 (快捷键 Ctrl/Cmd+B)',
|
|
1206
|
-
},
|
|
1966
|
+
}, h(IconClose)) : null
|
|
1207
1967
|
)
|
|
1208
1968
|
),
|
|
1209
1969
|
|
|
@@ -1283,7 +2043,40 @@
|
|
|
1283
2043
|
),
|
|
1284
2044
|
|
|
1285
2045
|
// Tree Content
|
|
1286
|
-
h('div', {
|
|
2046
|
+
h('div', {
|
|
2047
|
+
className: `dsh-tree-node-list ${isRootDropTarget ? 'drop-target-root' : ''}`,
|
|
2048
|
+
onContextMenu: (e) => {
|
|
2049
|
+
e.preventDefault();
|
|
2050
|
+
setContextMenu({ x: e.clientX, y: e.clientY, node: null, isRoot: true });
|
|
2051
|
+
},
|
|
2052
|
+
onDragOver: (e) => {
|
|
2053
|
+
if (draggedNode && isValidDropTarget(draggedNode, { path: rootPath, isDir: true })) {
|
|
2054
|
+
e.preventDefault();
|
|
2055
|
+
e.dataTransfer.dropEffect = 'move';
|
|
2056
|
+
setDragOverNode({ path: rootPath, isDir: true });
|
|
2057
|
+
}
|
|
2058
|
+
},
|
|
2059
|
+
onDragLeave: () => {
|
|
2060
|
+
if (dragOverNode && dragOverNode.path === rootPath) {
|
|
2061
|
+
setDragOverNode(null);
|
|
2062
|
+
}
|
|
2063
|
+
},
|
|
2064
|
+
onDrop: (e) => {
|
|
2065
|
+
e.preventDefault();
|
|
2066
|
+
setDragOverNode(null);
|
|
2067
|
+
if (draggedNode && isValidDropTarget(draggedNode, { path: rootPath, isDir: true })) {
|
|
2068
|
+
handleMove(draggedNode.path, rootPath);
|
|
2069
|
+
}
|
|
2070
|
+
},
|
|
2071
|
+
},
|
|
2072
|
+
// If creating at root level, render inline create input at top
|
|
2073
|
+
isCreatingAtRoot ? h(InlineCreateInputNode, {
|
|
2074
|
+
depth: 0,
|
|
2075
|
+
type: creatingItem.type,
|
|
2076
|
+
onConfirm: handleConfirmCreate,
|
|
2077
|
+
onCancel: () => setCreatingItem(null),
|
|
2078
|
+
}) : null,
|
|
2079
|
+
|
|
1287
2080
|
isLoading ? (
|
|
1288
2081
|
h('div', { className: 'dsh-tree-loading' }, '正在扫描工作区文件...')
|
|
1289
2082
|
) : error ? (
|
|
@@ -1295,7 +2088,7 @@
|
|
|
1295
2088
|
onClick: () => loadTree(true),
|
|
1296
2089
|
}, '重试')
|
|
1297
2090
|
)
|
|
1298
|
-
) : displayTree.length === 0 ? (
|
|
2091
|
+
) : displayTree.length === 0 && !isCreatingAtRoot ? (
|
|
1299
2092
|
h('div', { className: 'dsh-tree-empty' }, searchQuery ? '未匹配到相关文件' : '工作区暂无文件')
|
|
1300
2093
|
) : (
|
|
1301
2094
|
displayTree.map(node => h(FileTreeNodeItem, {
|
|
@@ -1307,10 +2100,42 @@
|
|
|
1307
2100
|
activeFilePath,
|
|
1308
2101
|
onOpenFile,
|
|
1309
2102
|
searchQuery,
|
|
2103
|
+
creatingItem,
|
|
2104
|
+
onConfirmCreate: handleConfirmCreate,
|
|
2105
|
+
onCancelCreate: () => setCreatingItem(null),
|
|
2106
|
+
renamingPath,
|
|
2107
|
+
onConfirmRename: handleConfirmRename,
|
|
2108
|
+
onCancelRename: () => setRenamingPath(null),
|
|
2109
|
+
onContextMenu: (targetNode, e) => {
|
|
2110
|
+
setContextMenu({ x: e.clientX, y: e.clientY, node: targetNode, isRoot: false });
|
|
2111
|
+
},
|
|
2112
|
+
draggedNode,
|
|
2113
|
+
dragOverNode,
|
|
2114
|
+
onDragStart: (targetNode) => setDraggedNode(targetNode),
|
|
2115
|
+
onDragEnd: () => { setDraggedNode(null); setDragOverNode(null); },
|
|
2116
|
+
onDragOver: (targetNode) => setDragOverNode(targetNode),
|
|
2117
|
+
onDragLeave: () => setDragOverNode(null),
|
|
2118
|
+
onDrop: (source, target) => {
|
|
2119
|
+
setDraggedNode(null);
|
|
2120
|
+
setDragOverNode(null);
|
|
2121
|
+
handleMove(source.path, target.path);
|
|
2122
|
+
},
|
|
2123
|
+
clipboard,
|
|
1310
2124
|
}))
|
|
1311
2125
|
)
|
|
1312
2126
|
),
|
|
1313
2127
|
|
|
2128
|
+
// Context Menu Overlay
|
|
2129
|
+
contextMenu ? h(TreeContextMenu, {
|
|
2130
|
+
x: contextMenu.x,
|
|
2131
|
+
y: contextMenu.y,
|
|
2132
|
+
node: contextMenu.node,
|
|
2133
|
+
isRoot: contextMenu.isRoot,
|
|
2134
|
+
clipboard,
|
|
2135
|
+
onAction: handleContextMenuAction,
|
|
2136
|
+
onClose: () => setContextMenu(null),
|
|
2137
|
+
}) : null,
|
|
2138
|
+
|
|
1314
2139
|
// Sidebar Footer with file count stats
|
|
1315
2140
|
treeData ? h('div', { className: 'dsh-tree-sidebar-footer' },
|
|
1316
2141
|
h('span', null, `${treeData.totalDirs || 0} 目录`),
|
|
@@ -2242,22 +3067,62 @@
|
|
|
2242
3067
|
}
|
|
2243
3068
|
|
|
2244
3069
|
// =========================================================================
|
|
2245
|
-
// React Component: CodeEditorView
|
|
3070
|
+
// React Component: CodeEditorView (Unified line-row architecture)
|
|
2246
3071
|
// =========================================================================
|
|
2247
|
-
function
|
|
2248
|
-
|
|
3072
|
+
function splitHighlightedHtmlIntoLines(html) {
|
|
3073
|
+
if (!html) return [];
|
|
3074
|
+
const rawLines = html.split('\n');
|
|
3075
|
+
const lines = [];
|
|
3076
|
+
const openTags = [];
|
|
3077
|
+
|
|
3078
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
3079
|
+
let line = rawLines[i];
|
|
3080
|
+
let prefix = '';
|
|
3081
|
+
if (openTags.length > 0) {
|
|
3082
|
+
prefix = openTags.map(t => '<' + t.name + (t.attrs ? ' ' + t.attrs : '') + '>').join('');
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
const tagRegex = /<\/?([a-zA-Z0-9\-]+)([^>]*)>/g;
|
|
3086
|
+
let match;
|
|
3087
|
+
while ((match = tagRegex.exec(line)) !== null) {
|
|
3088
|
+
const isClosing = match[0].startsWith('</');
|
|
3089
|
+
const tagName = match[1];
|
|
3090
|
+
const attrs = match[2];
|
|
3091
|
+
if (isClosing) {
|
|
3092
|
+
const lastIdx = openTags.map(t => t.name).lastIndexOf(tagName);
|
|
3093
|
+
if (lastIdx !== -1) {
|
|
3094
|
+
openTags.splice(lastIdx, 1);
|
|
3095
|
+
}
|
|
3096
|
+
} else if (!match[0].endsWith('/>')) {
|
|
3097
|
+
openTags.push({ name: tagName, attrs: attrs.trim() });
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
let suffix = '';
|
|
3102
|
+
if (openTags.length > 0) {
|
|
3103
|
+
suffix = openTags.slice().reverse().map(t => '</' + t.name + '>').join('');
|
|
3104
|
+
}
|
|
2249
3105
|
|
|
2250
|
-
|
|
3106
|
+
lines.push(prefix + line + suffix);
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
return lines;
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
function CodeEditorView({ content, category, isHighlighted = true, isWordWrap = true, searchQuery = '', onLineClick }) {
|
|
3113
|
+
const normalizedContent = useMemo(() => {
|
|
2251
3114
|
if (!content) return '';
|
|
3115
|
+
return content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
3116
|
+
}, [content]);
|
|
3117
|
+
|
|
3118
|
+
const highlightedLines = useMemo(() => {
|
|
3119
|
+
if (!normalizedContent) return [];
|
|
3120
|
+
let html = '';
|
|
2252
3121
|
if (!isHighlighted) {
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
escaped = escaped.replace(new RegExp(`(${q})`, 'gi'), '<mark class="dsh-search-highlight">$1</mark>');
|
|
2257
|
-
}
|
|
2258
|
-
return escaped;
|
|
3122
|
+
html = escapeHtml(normalizedContent);
|
|
3123
|
+
} else {
|
|
3124
|
+
html = highlightCode(normalizedContent, category);
|
|
2259
3125
|
}
|
|
2260
|
-
let html = highlightCode(content, category);
|
|
2261
3126
|
if (searchQuery.trim()) {
|
|
2262
3127
|
try {
|
|
2263
3128
|
const q = escapeHtml(searchQuery.trim());
|
|
@@ -2265,27 +3130,34 @@
|
|
|
2265
3130
|
html = html.replace(regex, '<mark class="dsh-search-highlight">$1</mark>');
|
|
2266
3131
|
} catch (e) {}
|
|
2267
3132
|
}
|
|
2268
|
-
return html;
|
|
2269
|
-
}, [
|
|
3133
|
+
return splitHighlightedHtmlIntoLines(html);
|
|
3134
|
+
}, [normalizedContent, category, isHighlighted, searchQuery]);
|
|
3135
|
+
|
|
3136
|
+
const lineCount = highlightedLines.length;
|
|
3137
|
+
const digitCount = Math.max(2, String(lineCount).length);
|
|
3138
|
+
const gutterWidth = Math.max(24, digitCount * 7.5 + 12);
|
|
2270
3139
|
|
|
2271
3140
|
return h('div', { className: `dsh-code-editor-view ${isWordWrap ? 'word-wrap' : ''}` },
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
3141
|
+
highlightedLines.map((lineHtml, i) => {
|
|
3142
|
+
const lineNum = i + 1;
|
|
3143
|
+
return h('div', {
|
|
3144
|
+
key: lineNum,
|
|
3145
|
+
id: `line-row-${lineNum}`,
|
|
3146
|
+
className: 'dsh-code-line',
|
|
3147
|
+
},
|
|
3148
|
+
h('span', {
|
|
3149
|
+
id: `line-gutter-${lineNum}`,
|
|
3150
|
+
className: 'dsh-line-gutter',
|
|
3151
|
+
style: { minWidth: `${gutterWidth}px` },
|
|
3152
|
+
onClick: () => onLineClick && onLineClick(lineNum),
|
|
3153
|
+
title: `第 ${lineNum} 行 (点击定位)`,
|
|
3154
|
+
}, lineNum),
|
|
3155
|
+
h('span', {
|
|
3156
|
+
className: 'dsh-line-code',
|
|
3157
|
+
dangerouslySetInnerHTML: { __html: lineHtml ? lineHtml : ' ' },
|
|
2286
3158
|
})
|
|
2287
|
-
)
|
|
2288
|
-
)
|
|
3159
|
+
);
|
|
3160
|
+
})
|
|
2289
3161
|
);
|
|
2290
3162
|
}
|
|
2291
3163
|
|
|
@@ -2387,7 +3259,9 @@
|
|
|
2387
3259
|
const el = contentRef.current.querySelector(`#${item.id}`);
|
|
2388
3260
|
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
2389
3261
|
} else {
|
|
2390
|
-
const
|
|
3262
|
+
const rowEl = document.getElementById(`line-row-${item.line}`);
|
|
3263
|
+
const gutterEl = document.getElementById(`line-gutter-${item.line}`);
|
|
3264
|
+
const el = rowEl || gutterEl;
|
|
2391
3265
|
if (el) {
|
|
2392
3266
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
2393
3267
|
el.classList.add('dsh-line-highlight-flash');
|
|
@@ -4505,6 +5379,7 @@
|
|
|
4505
5379
|
align-items: center;
|
|
4506
5380
|
gap: 2px;
|
|
4507
5381
|
flex-shrink: 0;
|
|
5382
|
+
white-space: nowrap;
|
|
4508
5383
|
}
|
|
4509
5384
|
|
|
4510
5385
|
.dsh-tree-action-btn {
|
|
@@ -4518,16 +5393,25 @@
|
|
|
4518
5393
|
display: inline-flex;
|
|
4519
5394
|
align-items: center;
|
|
4520
5395
|
justify-content: center;
|
|
4521
|
-
font-size: 11px;
|
|
4522
5396
|
padding: 0;
|
|
4523
|
-
|
|
5397
|
+
flex-shrink: 0;
|
|
5398
|
+
white-space: nowrap;
|
|
5399
|
+
transition: background 0.12s ease, color 0.12s ease;
|
|
5400
|
+
}
|
|
5401
|
+
body[data-ds-dark-theme] .dsh-tree-action-btn {
|
|
5402
|
+
color: #94a3b8;
|
|
5403
|
+
}
|
|
5404
|
+
.dsh-tree-action-btn svg {
|
|
5405
|
+
display: block;
|
|
5406
|
+
flex-shrink: 0;
|
|
5407
|
+
pointer-events: none;
|
|
4524
5408
|
}
|
|
4525
5409
|
.dsh-tree-action-btn:hover {
|
|
4526
|
-
background: rgba(0, 0, 0, 0.
|
|
5410
|
+
background: var(--dsw-alias-bg-hover, rgba(0, 0, 0, 0.07));
|
|
4527
5411
|
color: var(--dsw-alias-label-primary, #0f172a);
|
|
4528
5412
|
}
|
|
4529
5413
|
body[data-ds-dark-theme] .dsh-tree-action-btn:hover {
|
|
4530
|
-
background: rgba(255, 255, 255, 0.
|
|
5414
|
+
background: rgba(255, 255, 255, 0.1);
|
|
4531
5415
|
color: #ffffff;
|
|
4532
5416
|
}
|
|
4533
5417
|
|
|
@@ -4741,6 +5625,124 @@
|
|
|
4741
5625
|
}
|
|
4742
5626
|
.dsh-tree-footer-dot { font-size: 8px; }
|
|
4743
5627
|
|
|
5628
|
+
/* Drag & Drop & Cut Styles */
|
|
5629
|
+
.dsh-tree-node-row.drop-target {
|
|
5630
|
+
background: rgba(14, 165, 233, 0.22) !important;
|
|
5631
|
+
outline: 1px dashed #0284c7 !important;
|
|
5632
|
+
color: #0284c7 !important;
|
|
5633
|
+
}
|
|
5634
|
+
body[data-ds-dark-theme] .dsh-tree-node-row.drop-target {
|
|
5635
|
+
background: rgba(56, 189, 248, 0.25) !important;
|
|
5636
|
+
outline-color: #38bdf8 !important;
|
|
5637
|
+
color: #38bdf8 !important;
|
|
5638
|
+
}
|
|
5639
|
+
.dsh-tree-node-list.drop-target-root {
|
|
5640
|
+
background: rgba(14, 165, 233, 0.08) !important;
|
|
5641
|
+
outline: 2px dashed #0284c7 !important;
|
|
5642
|
+
outline-offset: -4px;
|
|
5643
|
+
}
|
|
5644
|
+
body[data-ds-dark-theme] .dsh-tree-node-list.drop-target-root {
|
|
5645
|
+
background: rgba(56, 189, 248, 0.1) !important;
|
|
5646
|
+
outline-color: #38bdf8 !important;
|
|
5647
|
+
}
|
|
5648
|
+
.dsh-tree-node-row.is-dragged {
|
|
5649
|
+
opacity: 0.45;
|
|
5650
|
+
filter: grayscale(0.6);
|
|
5651
|
+
}
|
|
5652
|
+
.dsh-tree-node-row.is-cut {
|
|
5653
|
+
opacity: 0.5;
|
|
5654
|
+
filter: grayscale(0.5);
|
|
5655
|
+
border: 1px dashed #94a3b8;
|
|
5656
|
+
}
|
|
5657
|
+
|
|
5658
|
+
/* Inline Creation & Rename Input */
|
|
5659
|
+
.dsh-tree-inline-input-row {
|
|
5660
|
+
display: flex;
|
|
5661
|
+
align-items: center;
|
|
5662
|
+
height: 24px;
|
|
5663
|
+
margin: 1px 4px;
|
|
5664
|
+
gap: 3px;
|
|
5665
|
+
box-sizing: border-box;
|
|
5666
|
+
}
|
|
5667
|
+
.dsh-tree-inline-input {
|
|
5668
|
+
flex: 1;
|
|
5669
|
+
height: 20px;
|
|
5670
|
+
border-radius: 3px;
|
|
5671
|
+
border: 1px solid #38bdf8;
|
|
5672
|
+
background: var(--dsw-alias-bg-base, #ffffff);
|
|
5673
|
+
color: inherit;
|
|
5674
|
+
font-size: 11px;
|
|
5675
|
+
padding: 0 4px;
|
|
5676
|
+
outline: none;
|
|
5677
|
+
box-sizing: border-box;
|
|
5678
|
+
box-shadow: 0 0 0 1px rgba(56, 189, 248, 0.4);
|
|
5679
|
+
}
|
|
5680
|
+
body[data-ds-dark-theme] .dsh-tree-inline-input {
|
|
5681
|
+
background: #1e2330;
|
|
5682
|
+
color: #f1f5f9;
|
|
5683
|
+
border-color: #38bdf8;
|
|
5684
|
+
}
|
|
5685
|
+
|
|
5686
|
+
/* Tree Context Menu */
|
|
5687
|
+
.dsh-tree-context-menu {
|
|
5688
|
+
position: fixed;
|
|
5689
|
+
min-width: 170px;
|
|
5690
|
+
background: var(--dsw-alias-bg-layer-1, #ffffff);
|
|
5691
|
+
border: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
|
|
5692
|
+
border-radius: 6px;
|
|
5693
|
+
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.2), 0 8px 10px -6px rgba(0, 0, 0, 0.15);
|
|
5694
|
+
padding: 4px;
|
|
5695
|
+
user-select: none;
|
|
5696
|
+
font-family: inherit;
|
|
5697
|
+
}
|
|
5698
|
+
body[data-ds-dark-theme] .dsh-tree-context-menu {
|
|
5699
|
+
background: #1a1e29;
|
|
5700
|
+
border-color: #2e384d;
|
|
5701
|
+
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5);
|
|
5702
|
+
}
|
|
5703
|
+
.dsh-menu-item {
|
|
5704
|
+
display: flex;
|
|
5705
|
+
align-items: center;
|
|
5706
|
+
gap: 8px;
|
|
5707
|
+
padding: 5px 8px;
|
|
5708
|
+
border-radius: 4px;
|
|
5709
|
+
font-size: 12px;
|
|
5710
|
+
cursor: pointer;
|
|
5711
|
+
color: var(--dsw-alias-label-primary, #1e293b);
|
|
5712
|
+
transition: background 0.1s;
|
|
5713
|
+
}
|
|
5714
|
+
body[data-ds-dark-theme] .dsh-menu-item {
|
|
5715
|
+
color: #e2e8f0;
|
|
5716
|
+
}
|
|
5717
|
+
.dsh-menu-item:hover {
|
|
5718
|
+
background: var(--dsw-alias-bg-hover, rgba(0, 0, 0, 0.06));
|
|
5719
|
+
}
|
|
5720
|
+
body[data-ds-dark-theme] .dsh-menu-item:hover {
|
|
5721
|
+
background: rgba(255, 255, 255, 0.09);
|
|
5722
|
+
}
|
|
5723
|
+
.dsh-menu-item-danger {
|
|
5724
|
+
color: #ef4444 !important;
|
|
5725
|
+
}
|
|
5726
|
+
.dsh-menu-item-danger:hover {
|
|
5727
|
+
background: rgba(239, 68, 68, 0.12) !important;
|
|
5728
|
+
}
|
|
5729
|
+
.dsh-menu-icon {
|
|
5730
|
+
font-size: 13px;
|
|
5731
|
+
width: 16px;
|
|
5732
|
+
display: inline-flex;
|
|
5733
|
+
align-items: center;
|
|
5734
|
+
justify-content: center;
|
|
5735
|
+
flex-shrink: 0;
|
|
5736
|
+
}
|
|
5737
|
+
.dsh-menu-divider {
|
|
5738
|
+
height: 1px;
|
|
5739
|
+
background: var(--dsw-alias-border-l3, #e2e8f0);
|
|
5740
|
+
margin: 3px 4px;
|
|
5741
|
+
}
|
|
5742
|
+
body[data-ds-dark-theme] .dsh-menu-divider {
|
|
5743
|
+
background: #2a3140;
|
|
5744
|
+
}
|
|
5745
|
+
|
|
4744
5746
|
/* Splitter between Tree and Preview */
|
|
4745
5747
|
.dsh-tree-splitter {
|
|
4746
5748
|
width: 4px;
|
|
@@ -4860,85 +5862,89 @@
|
|
|
4860
5862
|
body[data-ds-dark-theme] .tok-prop { color: #9cdcfe; }
|
|
4861
5863
|
body[data-ds-dark-theme] .tok-selector { color: #d7ba7d; }
|
|
4862
5864
|
|
|
4863
|
-
/* Code Editor Viewer */
|
|
5865
|
+
/* Code Editor Viewer (with unified line row architecture for 100% perfect alignment) */
|
|
4864
5866
|
.dsh-code-editor-view {
|
|
4865
5867
|
display: flex;
|
|
5868
|
+
flex-direction: column;
|
|
5869
|
+
width: 100%;
|
|
4866
5870
|
min-height: 100%;
|
|
4867
|
-
|
|
4868
|
-
font-
|
|
5871
|
+
overflow: auto;
|
|
5872
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Cascadia Code", "Fira Code", monospace;
|
|
5873
|
+
font-size: 12px;
|
|
4869
5874
|
line-height: 20px;
|
|
4870
5875
|
background: var(--dsw-alias-bg-layer-1, #ffffff);
|
|
4871
5876
|
color: var(--dsw-alias-label-primary, #1e293b);
|
|
5877
|
+
padding: 8px 0;
|
|
5878
|
+
box-sizing: border-box;
|
|
5879
|
+
user-select: text;
|
|
4872
5880
|
}
|
|
4873
5881
|
body[data-ds-dark-theme] .dsh-code-editor-view {
|
|
4874
|
-
background: #
|
|
5882
|
+
background: #161922;
|
|
4875
5883
|
color: #d4d4d4;
|
|
4876
5884
|
}
|
|
4877
5885
|
|
|
4878
|
-
.dsh-code-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
5886
|
+
.dsh-code-line {
|
|
5887
|
+
display: flex;
|
|
5888
|
+
flex-direction: row;
|
|
5889
|
+
align-items: flex-start;
|
|
5890
|
+
min-height: 20px;
|
|
5891
|
+
line-height: 20px;
|
|
5892
|
+
width: 100%;
|
|
5893
|
+
box-sizing: border-box;
|
|
5894
|
+
transition: background 0.1s;
|
|
5895
|
+
}
|
|
5896
|
+
.dsh-code-line:hover {
|
|
5897
|
+
background: rgba(0, 0, 0, 0.035);
|
|
5898
|
+
}
|
|
5899
|
+
body[data-ds-dark-theme] .dsh-code-line:hover {
|
|
5900
|
+
background: rgba(255, 255, 255, 0.035);
|
|
5901
|
+
}
|
|
5902
|
+
.dsh-code-line.dsh-line-highlight-flash {
|
|
5903
|
+
background: rgba(14, 165, 233, 0.22) !important;
|
|
5904
|
+
}
|
|
5905
|
+
|
|
5906
|
+
.dsh-line-gutter {
|
|
5907
|
+
display: inline-block;
|
|
5908
|
+
box-sizing: border-box;
|
|
5909
|
+
padding: 0 8px 0 6px;
|
|
4882
5910
|
text-align: right;
|
|
4883
5911
|
user-select: none;
|
|
4884
|
-
background: var(--dsw-alias-bg-layer-2, #f8fafc);
|
|
4885
|
-
border-right: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
|
|
4886
5912
|
color: var(--dsw-alias-label-tertiary, #94a3b8);
|
|
4887
|
-
font-size:
|
|
5913
|
+
font-size: 11px;
|
|
4888
5914
|
line-height: 20px;
|
|
4889
5915
|
flex-shrink: 0;
|
|
5916
|
+
border-right: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
|
|
5917
|
+
cursor: pointer;
|
|
5918
|
+
transition: color 0.1s;
|
|
4890
5919
|
}
|
|
4891
|
-
body[data-ds-dark-theme] .dsh-
|
|
4892
|
-
background: #141720;
|
|
5920
|
+
body[data-ds-dark-theme] .dsh-line-gutter {
|
|
4893
5921
|
border-right-color: #2a3140;
|
|
4894
|
-
color: #
|
|
4895
|
-
}
|
|
4896
|
-
|
|
4897
|
-
.dsh-gutter-line {
|
|
4898
|
-
height: 20px;
|
|
4899
|
-
cursor: pointer;
|
|
4900
|
-
padding-right: 8px;
|
|
5922
|
+
color: #55637a;
|
|
4901
5923
|
}
|
|
4902
|
-
.dsh-gutter
|
|
5924
|
+
.dsh-line-gutter:hover {
|
|
4903
5925
|
color: var(--dsw-brand-primary, #0284c7);
|
|
4904
|
-
font-weight:
|
|
5926
|
+
font-weight: 600;
|
|
4905
5927
|
}
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
color: #0284c7;
|
|
5928
|
+
body[data-ds-dark-theme] .dsh-line-gutter:hover {
|
|
5929
|
+
color: #38bdf8;
|
|
4909
5930
|
}
|
|
4910
5931
|
|
|
4911
|
-
.dsh-code
|
|
5932
|
+
.dsh-line-code {
|
|
4912
5933
|
flex: 1;
|
|
4913
5934
|
min-width: 0;
|
|
4914
|
-
padding: 12px
|
|
4915
|
-
|
|
4916
|
-
}
|
|
4917
|
-
.dsh-code-pre {
|
|
4918
|
-
margin: 0;
|
|
5935
|
+
padding: 0 12px 0 10px;
|
|
5936
|
+
white-space: pre;
|
|
4919
5937
|
font-family: inherit;
|
|
4920
5938
|
font-size: inherit;
|
|
4921
5939
|
line-height: 20px;
|
|
4922
5940
|
tab-size: 2;
|
|
4923
|
-
|
|
5941
|
+
box-sizing: border-box;
|
|
4924
5942
|
}
|
|
4925
|
-
.dsh-code-editor-view.word-wrap .dsh-code
|
|
5943
|
+
.dsh-code-editor-view.word-wrap .dsh-line-code {
|
|
4926
5944
|
white-space: pre-wrap;
|
|
4927
5945
|
word-break: normal;
|
|
4928
5946
|
overflow-wrap: break-word;
|
|
4929
5947
|
}
|
|
4930
|
-
.dsh-code-editor-view {
|
|
4931
|
-
display: flex;
|
|
4932
|
-
min-height: 100%;
|
|
4933
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
4934
|
-
font-size: 12.5px;
|
|
4935
|
-
line-height: 20px;
|
|
4936
|
-
background: var(--dsw-alias-bg-layer-1, #ffffff);
|
|
4937
|
-
color: var(--dsw-alias-label-primary, #1e293b);
|
|
4938
|
-
}
|
|
4939
|
-
.dsh-code-pre code {
|
|
4940
|
-
font-family: inherit;
|
|
4941
|
-
}
|
|
4942
5948
|
|
|
4943
5949
|
/* Interactive JSON Inspector */
|
|
4944
5950
|
.dsh-json-inspector {
|