@rooode/dsh-plugin-preview 0.1.10 → 0.1.11
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 +935 -92
- 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,262 @@
|
|
|
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 isValidDropTarget(draggedItem, targetNode) {
|
|
1047
|
+
if (!draggedItem || !targetNode) return false;
|
|
1048
|
+
if (!targetNode.isDir) return false;
|
|
1049
|
+
const src = draggedItem.path.replace(/\\/g, '/').toLowerCase();
|
|
1050
|
+
const dest = targetNode.path.replace(/\\/g, '/').toLowerCase();
|
|
1051
|
+
// Cannot drop on itself
|
|
1052
|
+
if (src === dest) return false;
|
|
1053
|
+
// Cannot drop into current parent directory (already there)
|
|
1054
|
+
const lastSlash = src.lastIndexOf('/');
|
|
1055
|
+
const srcParent = lastSlash > 0 ? src.substring(0, lastSlash) : '';
|
|
1056
|
+
if (srcParent && srcParent === dest) return false;
|
|
1057
|
+
// If dragged is a dir, cannot drop into its own children or itself
|
|
1058
|
+
if (draggedItem.isDir) {
|
|
1059
|
+
if (dest.startsWith(src + '/')) return false;
|
|
1060
|
+
}
|
|
1061
|
+
return true;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function InlineCreateInputNode({ depth = 0, type, onConfirm, onCancel }) {
|
|
1065
|
+
const [val, setVal] = useState('');
|
|
1066
|
+
const inputRef = useRef(null);
|
|
1067
|
+
|
|
1068
|
+
useEffect(() => {
|
|
1069
|
+
if (inputRef.current) {
|
|
1070
|
+
inputRef.current.focus();
|
|
1071
|
+
inputRef.current.select();
|
|
1072
|
+
}
|
|
1073
|
+
}, []);
|
|
1074
|
+
|
|
1075
|
+
const handleKeyDown = (e) => {
|
|
1076
|
+
if (e.key === 'Enter') {
|
|
1077
|
+
e.preventDefault();
|
|
1078
|
+
e.stopPropagation();
|
|
1079
|
+
if (val.trim()) onConfirm(val.trim());
|
|
1080
|
+
else onCancel();
|
|
1081
|
+
} else if (e.key === 'Escape') {
|
|
1082
|
+
e.preventDefault();
|
|
1083
|
+
e.stopPropagation();
|
|
1084
|
+
onCancel();
|
|
1085
|
+
}
|
|
1086
|
+
};
|
|
1087
|
+
|
|
1088
|
+
const handleBlur = () => {
|
|
1089
|
+
if (!val.trim()) onCancel();
|
|
1090
|
+
};
|
|
1091
|
+
|
|
1092
|
+
return h('div', {
|
|
1093
|
+
className: 'dsh-tree-inline-input-row',
|
|
1094
|
+
style: { paddingLeft: `${depth * 14 + 6}px` },
|
|
1095
|
+
onClick: (e) => e.stopPropagation(),
|
|
1096
|
+
},
|
|
1097
|
+
h('span', { className: 'dsh-tree-chevron-placeholder' }),
|
|
1098
|
+
h('span', {
|
|
1099
|
+
className: 'dsh-tree-icon',
|
|
1100
|
+
style: { color: type === 'dir' ? '#eab308' : '#38bdf8' },
|
|
1101
|
+
}, type === 'dir' ? '📁' : '📄'),
|
|
1102
|
+
h('input', {
|
|
1103
|
+
ref: inputRef,
|
|
1104
|
+
type: 'text',
|
|
1105
|
+
className: 'dsh-tree-inline-input',
|
|
1106
|
+
value: val,
|
|
1107
|
+
placeholder: type === 'dir' ? '新文件夹名称...' : '新文件名 (如 doc.md)...',
|
|
1108
|
+
onChange: (e) => setVal(e.target.value),
|
|
1109
|
+
onKeyDown: handleKeyDown,
|
|
1110
|
+
onBlur: handleBlur,
|
|
1111
|
+
})
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
function InlineRenameInputNode({ node, depth = 0, onConfirm, onCancel }) {
|
|
1116
|
+
const [val, setVal] = useState(node.name);
|
|
1117
|
+
const inputRef = useRef(null);
|
|
1118
|
+
|
|
1119
|
+
useEffect(() => {
|
|
1120
|
+
if (inputRef.current) {
|
|
1121
|
+
inputRef.current.focus();
|
|
1122
|
+
if (!node.isDir && node.name.includes('.')) {
|
|
1123
|
+
const lastDot = node.name.lastIndexOf('.');
|
|
1124
|
+
if (lastDot > 0) {
|
|
1125
|
+
inputRef.current.setSelectionRange(0, lastDot);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
inputRef.current.select();
|
|
1130
|
+
}
|
|
1131
|
+
}, [node]);
|
|
1132
|
+
|
|
1133
|
+
const handleKeyDown = (e) => {
|
|
1134
|
+
if (e.key === 'Enter') {
|
|
1135
|
+
e.preventDefault();
|
|
1136
|
+
e.stopPropagation();
|
|
1137
|
+
if (val.trim() && val.trim() !== node.name) {
|
|
1138
|
+
onConfirm(val.trim());
|
|
1139
|
+
} else {
|
|
1140
|
+
onCancel();
|
|
1141
|
+
}
|
|
1142
|
+
} else if (e.key === 'Escape') {
|
|
1143
|
+
e.preventDefault();
|
|
1144
|
+
e.stopPropagation();
|
|
1145
|
+
onCancel();
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1149
|
+
const handleBlur = () => {
|
|
1150
|
+
onCancel();
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1153
|
+
const iconInfo = getFileIcon(node);
|
|
1154
|
+
|
|
1155
|
+
return h('div', {
|
|
1156
|
+
className: 'dsh-tree-inline-input-row',
|
|
1157
|
+
style: { paddingLeft: `${depth * 14 + 6}px` },
|
|
1158
|
+
onClick: (e) => e.stopPropagation(),
|
|
1159
|
+
},
|
|
1160
|
+
h('span', { className: 'dsh-tree-chevron-placeholder' }),
|
|
1161
|
+
h('span', {
|
|
1162
|
+
className: 'dsh-tree-icon',
|
|
1163
|
+
style: { color: node.isDir ? '#eab308' : iconInfo.color },
|
|
1164
|
+
}, node.isDir ? '📁' : iconInfo.icon),
|
|
1165
|
+
h('input', {
|
|
1166
|
+
ref: inputRef,
|
|
1167
|
+
type: 'text',
|
|
1168
|
+
className: 'dsh-tree-inline-input',
|
|
1169
|
+
value: val,
|
|
1170
|
+
onChange: (e) => setVal(e.target.value),
|
|
1171
|
+
onKeyDown: handleKeyDown,
|
|
1172
|
+
onBlur: handleBlur,
|
|
1173
|
+
})
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function TreeContextMenu({ x, y, node, isRoot, clipboard, onAction, onClose }) {
|
|
1178
|
+
const menuRef = useRef(null);
|
|
1179
|
+
useEffect(() => {
|
|
1180
|
+
const handleDown = (e) => {
|
|
1181
|
+
if (menuRef.current && !menuRef.current.contains(e.target)) {
|
|
1182
|
+
onClose();
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
window.addEventListener('mousedown', handleDown, true);
|
|
1186
|
+
window.addEventListener('scroll', onClose, true);
|
|
1187
|
+
return () => {
|
|
1188
|
+
window.removeEventListener('mousedown', handleDown, true);
|
|
1189
|
+
window.removeEventListener('scroll', onClose, true);
|
|
1190
|
+
};
|
|
1191
|
+
}, [onClose]);
|
|
1192
|
+
|
|
1193
|
+
const style = {
|
|
1194
|
+
position: 'fixed',
|
|
1195
|
+
top: `${Math.min(Math.max(10, y), window.innerHeight - 300)}px`,
|
|
1196
|
+
left: `${Math.min(Math.max(10, x), window.innerWidth - 210)}px`,
|
|
1197
|
+
zIndex: 99999,
|
|
1198
|
+
};
|
|
1199
|
+
|
|
1200
|
+
const isDir = isRoot || (node && node.isDir);
|
|
1201
|
+
|
|
1202
|
+
return h('div', { ref: menuRef, className: 'dsh-tree-context-menu', style },
|
|
1203
|
+
isDir ? [
|
|
1204
|
+
h('div', { key: 'new-file', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('new-file', node); } },
|
|
1205
|
+
h('span', { className: 'dsh-menu-icon' }, '📄'),
|
|
1206
|
+
h('span', null, '新建文件')
|
|
1207
|
+
),
|
|
1208
|
+
h('div', { key: 'new-dir', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('new-dir', node); } },
|
|
1209
|
+
h('span', { className: 'dsh-menu-icon' }, '📁'),
|
|
1210
|
+
h('span', null, '新建文件夹')
|
|
1211
|
+
),
|
|
1212
|
+
h('div', { key: 'div-1', className: 'dsh-menu-divider' }),
|
|
1213
|
+
] : null,
|
|
1214
|
+
|
|
1215
|
+
!isRoot && !isDir ? h('div', { key: 'preview', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('preview', node); } },
|
|
1216
|
+
h('span', { className: 'dsh-menu-icon' }, '📖'),
|
|
1217
|
+
h('span', null, '打开预览')
|
|
1218
|
+
) : null,
|
|
1219
|
+
|
|
1220
|
+
!isRoot ? h('div', { key: 'cut', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('cut', node); } },
|
|
1221
|
+
h('span', { className: 'dsh-menu-icon' }, '✂️'),
|
|
1222
|
+
h('span', null, '剪切')
|
|
1223
|
+
) : null,
|
|
1224
|
+
|
|
1225
|
+
isDir && clipboard && clipboard.action === 'cut' ? h('div', {
|
|
1226
|
+
key: 'paste',
|
|
1227
|
+
className: 'dsh-menu-item',
|
|
1228
|
+
onClick: () => { onClose(); onAction('paste', node); }
|
|
1229
|
+
},
|
|
1230
|
+
h('span', { className: 'dsh-menu-icon' }, '📋'),
|
|
1231
|
+
h('span', null, `粘贴 "${clipboard.name}" 到此处`)
|
|
1232
|
+
) : null,
|
|
1233
|
+
|
|
1234
|
+
!isRoot ? h('div', { key: 'rename', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('rename', node); } },
|
|
1235
|
+
h('span', { className: 'dsh-menu-icon' }, '🏷️'),
|
|
1236
|
+
h('span', null, '重命名')
|
|
1237
|
+
) : null,
|
|
1238
|
+
|
|
1239
|
+
!isRoot ? h('div', { key: 'div-2', className: 'dsh-menu-divider' }) : null,
|
|
1240
|
+
|
|
1241
|
+
!isRoot ? h('div', { key: 'reveal', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('reveal', node); } },
|
|
1242
|
+
h('span', { className: 'dsh-menu-icon' }, '📂'),
|
|
1243
|
+
h('span', null, '在文件管理器中定位')
|
|
1244
|
+
) : null,
|
|
1245
|
+
|
|
1246
|
+
!isRoot && !isDir ? h('div', { key: 'native', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('native', node); } },
|
|
1247
|
+
h('span', { className: 'dsh-menu-icon' }, '🌐'),
|
|
1248
|
+
h('span', null, '系统默认应用打开')
|
|
1249
|
+
) : null,
|
|
1250
|
+
|
|
1251
|
+
!isRoot ? [
|
|
1252
|
+
h('div', { key: 'div-3', className: 'dsh-menu-divider' }),
|
|
1253
|
+
h('div', { key: 'delete', className: 'dsh-menu-item dsh-menu-item-danger', onClick: () => { onClose(); onAction('delete', node); } },
|
|
1254
|
+
h('span', { className: 'dsh-menu-icon' }, '🗑️'),
|
|
1255
|
+
h('span', null, '删除')
|
|
1256
|
+
)
|
|
1257
|
+
] : null,
|
|
1258
|
+
|
|
1259
|
+
isRoot ? [
|
|
1260
|
+
h('div', { key: 'refresh', className: 'dsh-menu-item', onClick: () => { onClose(); onAction('refresh'); } },
|
|
1261
|
+
h('span', { className: 'dsh-menu-icon' }, '🔄'),
|
|
1262
|
+
h('span', null, '刷新目录树')
|
|
1263
|
+
)
|
|
1264
|
+
] : null
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
function FileTreeNodeItem({
|
|
1269
|
+
node,
|
|
1270
|
+
depth = 0,
|
|
1271
|
+
expandedPaths,
|
|
1272
|
+
onToggleExpand,
|
|
1273
|
+
activeFilePath,
|
|
1274
|
+
onOpenFile,
|
|
1275
|
+
searchQuery,
|
|
1276
|
+
creatingItem,
|
|
1277
|
+
onConfirmCreate,
|
|
1278
|
+
onCancelCreate,
|
|
1279
|
+
renamingPath,
|
|
1280
|
+
onConfirmRename,
|
|
1281
|
+
onCancelRename,
|
|
1282
|
+
onContextMenu,
|
|
1283
|
+
draggedNode,
|
|
1284
|
+
dragOverNode,
|
|
1285
|
+
onDragStart,
|
|
1286
|
+
onDragEnd,
|
|
1287
|
+
onDragOver,
|
|
1288
|
+
onDragLeave,
|
|
1289
|
+
onDrop,
|
|
1290
|
+
clipboard,
|
|
1291
|
+
}) {
|
|
955
1292
|
const isDir = node.isDir;
|
|
956
1293
|
const isExpanded = expandedPaths.has(node.path);
|
|
957
1294
|
const isActive = !isDir && activeFilePath && activeFilePath.toLowerCase() === node.path.toLowerCase();
|
|
1295
|
+
const isRenaming = renamingPath && renamingPath.toLowerCase() === node.path.toLowerCase();
|
|
1296
|
+
const isDragged = draggedNode && draggedNode.path && draggedNode.path.toLowerCase() === node.path.toLowerCase();
|
|
1297
|
+
const isDropTarget = isDir && dragOverNode && dragOverNode.path && dragOverNode.path.toLowerCase() === node.path.toLowerCase();
|
|
1298
|
+
const isCut = clipboard && clipboard.action === 'cut' && clipboard.path && clipboard.path.toLowerCase() === node.path.toLowerCase();
|
|
958
1299
|
const iconInfo = getFileIcon(node);
|
|
959
1300
|
|
|
960
1301
|
const indentStyle = { paddingLeft: `${depth * 14 + 6}px` };
|
|
@@ -968,16 +1309,70 @@
|
|
|
968
1309
|
}
|
|
969
1310
|
};
|
|
970
1311
|
|
|
1312
|
+
const handleContextMenu = (e) => {
|
|
1313
|
+
e.preventDefault();
|
|
1314
|
+
e.stopPropagation();
|
|
1315
|
+
onContextMenu(node, e);
|
|
1316
|
+
};
|
|
1317
|
+
|
|
1318
|
+
const handleDragStart = (e) => {
|
|
1319
|
+
e.stopPropagation();
|
|
1320
|
+
e.dataTransfer.setData('text/plain', node.path);
|
|
1321
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
1322
|
+
onDragStart(node);
|
|
1323
|
+
};
|
|
1324
|
+
|
|
1325
|
+
const handleDragOver = (e) => {
|
|
1326
|
+
if (isDir && isValidDropTarget(draggedNode, node)) {
|
|
1327
|
+
e.preventDefault();
|
|
1328
|
+
e.stopPropagation();
|
|
1329
|
+
e.dataTransfer.dropEffect = 'move';
|
|
1330
|
+
onDragOver(node);
|
|
1331
|
+
}
|
|
1332
|
+
};
|
|
1333
|
+
|
|
1334
|
+
const handleDragLeave = (e) => {
|
|
1335
|
+
if (isDir) {
|
|
1336
|
+
e.stopPropagation();
|
|
1337
|
+
onDragLeave(node);
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
|
|
1341
|
+
const handleDrop = (e) => {
|
|
1342
|
+
if (isDir && isValidDropTarget(draggedNode, node)) {
|
|
1343
|
+
e.preventDefault();
|
|
1344
|
+
e.stopPropagation();
|
|
1345
|
+
onDrop(draggedNode, node);
|
|
1346
|
+
}
|
|
1347
|
+
};
|
|
1348
|
+
|
|
1349
|
+
if (isRenaming) {
|
|
1350
|
+
return h(InlineRenameInputNode, {
|
|
1351
|
+
node,
|
|
1352
|
+
depth,
|
|
1353
|
+
onConfirm: (newName) => onConfirmRename(node, newName),
|
|
1354
|
+
onCancel: onCancelRename,
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
|
|
971
1358
|
return h(Fragment, null,
|
|
972
1359
|
h('div', {
|
|
973
|
-
className: `dsh-tree-node-row ${isDir ? 'is-dir' : 'is-file'} ${isActive ? 'active' : ''}`,
|
|
1360
|
+
className: `dsh-tree-node-row ${isDir ? 'is-dir' : 'is-file'} ${isActive ? 'active' : ''} ${isDropTarget ? 'drop-target' : ''} ${isDragged ? 'is-dragged' : ''} ${isCut ? 'is-cut' : ''}`,
|
|
974
1361
|
style: indentStyle,
|
|
975
1362
|
onClick: handleClick,
|
|
1363
|
+
onContextMenu: handleContextMenu,
|
|
1364
|
+
draggable: true,
|
|
1365
|
+
onDragStart: handleDragStart,
|
|
1366
|
+
onDragEnd: onDragEnd,
|
|
1367
|
+
onDragOver: handleDragOver,
|
|
1368
|
+
onDragLeave: handleDragLeave,
|
|
1369
|
+
onDrop: handleDrop,
|
|
976
1370
|
title: `${node.name}\n${node.path}${node.size ? ' (' + formatBytes(node.size) + ')' : ''}`,
|
|
977
1371
|
},
|
|
978
1372
|
// Chevron indicator for directory
|
|
979
1373
|
isDir ? h('span', {
|
|
980
1374
|
className: `dsh-tree-chevron ${isExpanded ? 'expanded' : ''}`,
|
|
1375
|
+
onClick: (e) => { e.stopPropagation(); onToggleExpand(node.path); },
|
|
981
1376
|
}, isExpanded ? '▼' : '▶') : h('span', { className: 'dsh-tree-chevron-placeholder' }),
|
|
982
1377
|
|
|
983
1378
|
// File / Directory Icon
|
|
@@ -995,18 +1390,47 @@
|
|
|
995
1390
|
)
|
|
996
1391
|
),
|
|
997
1392
|
|
|
998
|
-
// Children if expanded
|
|
999
|
-
isDir && isExpanded
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
node
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1393
|
+
// Children or Inline creation node if expanded
|
|
1394
|
+
isDir && isExpanded ? (
|
|
1395
|
+
h(Fragment, null,
|
|
1396
|
+
// If creating inside this directory, render inline creation node first
|
|
1397
|
+
creatingItem && creatingItem.parentPath && creatingItem.parentPath.replace(/\\/g, '/').toLowerCase() === node.path.replace(/\\/g, '/').toLowerCase() ? (
|
|
1398
|
+
h(InlineCreateInputNode, {
|
|
1399
|
+
depth: depth + 1,
|
|
1400
|
+
type: creatingItem.type,
|
|
1401
|
+
onConfirm: onConfirmCreate,
|
|
1402
|
+
onCancel: onCancelCreate,
|
|
1403
|
+
})
|
|
1404
|
+
) : null,
|
|
1405
|
+
|
|
1406
|
+
node.children && node.children.length > 0 ? (
|
|
1407
|
+
node.children.map(child => h(FileTreeNodeItem, {
|
|
1408
|
+
key: child.path,
|
|
1409
|
+
node: child,
|
|
1410
|
+
depth: depth + 1,
|
|
1411
|
+
expandedPaths,
|
|
1412
|
+
onToggleExpand,
|
|
1413
|
+
activeFilePath,
|
|
1414
|
+
onOpenFile,
|
|
1415
|
+
searchQuery,
|
|
1416
|
+
creatingItem,
|
|
1417
|
+
onConfirmCreate,
|
|
1418
|
+
onCancelCreate,
|
|
1419
|
+
renamingPath,
|
|
1420
|
+
onConfirmRename,
|
|
1421
|
+
onCancelRename,
|
|
1422
|
+
onContextMenu,
|
|
1423
|
+
draggedNode,
|
|
1424
|
+
dragOverNode,
|
|
1425
|
+
onDragStart,
|
|
1426
|
+
onDragEnd,
|
|
1427
|
+
onDragOver,
|
|
1428
|
+
onDragLeave,
|
|
1429
|
+
onDrop,
|
|
1430
|
+
clipboard,
|
|
1431
|
+
}))
|
|
1432
|
+
) : null
|
|
1433
|
+
)
|
|
1010
1434
|
) : null
|
|
1011
1435
|
);
|
|
1012
1436
|
}
|
|
@@ -1060,6 +1484,15 @@
|
|
|
1060
1484
|
const [isSelectorOpen, setIsSelectorOpen] = useState(false);
|
|
1061
1485
|
const [customPathInput, setCustomPathInput] = useState('');
|
|
1062
1486
|
const [validating, setValidating] = useState(false);
|
|
1487
|
+
|
|
1488
|
+
// File operations state
|
|
1489
|
+
const [creatingItem, setCreatingItem] = useState(null); // { parentPath: string, type: 'dir' | 'file' } | null
|
|
1490
|
+
const [renamingPath, setRenamingPath] = useState(null); // string | null
|
|
1491
|
+
const [contextMenu, setContextMenu] = useState(null); // { x: number, y: number, node: any, isRoot: boolean } | null
|
|
1492
|
+
const [draggedNode, setDraggedNode] = useState(null);
|
|
1493
|
+
const [dragOverNode, setDragOverNode] = useState(null);
|
|
1494
|
+
const [clipboard, setClipboard] = useState(null); // { action: 'cut', path: string, name: string, isDir: boolean } | null
|
|
1495
|
+
|
|
1063
1496
|
const currentWs = getCurrentWorkspaceRoot();
|
|
1064
1497
|
|
|
1065
1498
|
const loadTree = useCallback(async (force = false) => {
|
|
@@ -1164,6 +1597,168 @@
|
|
|
1164
1597
|
showToast('已恢复根据当前会话自动识别工作空间', 'info');
|
|
1165
1598
|
};
|
|
1166
1599
|
|
|
1600
|
+
// =========================================================================
|
|
1601
|
+
// File Operations Handlers
|
|
1602
|
+
// =========================================================================
|
|
1603
|
+
const handleStartCreate = (parentPath, type) => {
|
|
1604
|
+
const root = treeData?.workspaceRoot || currentWs;
|
|
1605
|
+
const target = parentPath || root;
|
|
1606
|
+
setCreatingItem({ parentPath: target, type });
|
|
1607
|
+
if (target) {
|
|
1608
|
+
setExpandedPaths(prev => new Set(prev).add(target));
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
|
|
1612
|
+
const handleConfirmCreate = async (name) => {
|
|
1613
|
+
if (!creatingItem || !name) return;
|
|
1614
|
+
const isDir = creatingItem.type === 'dir';
|
|
1615
|
+
const endpoint = isDir ? '/api/preview/create-dir' : '/api/preview/create-file';
|
|
1616
|
+
try {
|
|
1617
|
+
const res = await fetch(endpoint, {
|
|
1618
|
+
method: 'POST',
|
|
1619
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1620
|
+
body: JSON.stringify({
|
|
1621
|
+
parentPath: creatingItem.parentPath,
|
|
1622
|
+
name: name.trim(),
|
|
1623
|
+
}),
|
|
1624
|
+
});
|
|
1625
|
+
const json = await res.json();
|
|
1626
|
+
if (json.ok) {
|
|
1627
|
+
setCreatingItem(null);
|
|
1628
|
+
setExpandedPaths(prev => new Set(prev).add(creatingItem.parentPath));
|
|
1629
|
+
await loadTree(true);
|
|
1630
|
+
showToast(`已创建${isDir ? '文件夹' : '文件'}: ${json.name}`, 'success');
|
|
1631
|
+
if (!isDir && json.path) {
|
|
1632
|
+
openPreviewFile(json.path);
|
|
1633
|
+
}
|
|
1634
|
+
} else {
|
|
1635
|
+
showToast(json?.error?.message || `创建${isDir ? '文件夹' : '文件'}失败`, 'error');
|
|
1636
|
+
}
|
|
1637
|
+
} catch (err) {
|
|
1638
|
+
showToast(`创建失败: ${err.message}`, 'error');
|
|
1639
|
+
}
|
|
1640
|
+
};
|
|
1641
|
+
|
|
1642
|
+
const handleConfirmRename = async (node, newName) => {
|
|
1643
|
+
if (!node || !newName || newName.trim() === node.name) {
|
|
1644
|
+
setRenamingPath(null);
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
try {
|
|
1648
|
+
const res = await fetch('/api/preview/rename', {
|
|
1649
|
+
method: 'POST',
|
|
1650
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1651
|
+
body: JSON.stringify({
|
|
1652
|
+
path: node.path,
|
|
1653
|
+
newName: newName.trim(),
|
|
1654
|
+
}),
|
|
1655
|
+
});
|
|
1656
|
+
const json = await res.json();
|
|
1657
|
+
if (json.ok) {
|
|
1658
|
+
setRenamingPath(null);
|
|
1659
|
+
renameOpenTabPaths(json.sourcePath, json.targetPath, node.isDir);
|
|
1660
|
+
await loadTree(true);
|
|
1661
|
+
showToast(`已重命名为: ${json.name}`, 'success');
|
|
1662
|
+
} else {
|
|
1663
|
+
showToast(json?.error?.message || '重命名失败', 'error');
|
|
1664
|
+
}
|
|
1665
|
+
} catch (err) {
|
|
1666
|
+
showToast(`重命名失败: ${err.message}`, 'error');
|
|
1667
|
+
}
|
|
1668
|
+
};
|
|
1669
|
+
|
|
1670
|
+
const handleMove = async (sourcePath, targetDirPath) => {
|
|
1671
|
+
if (!sourcePath || !targetDirPath) return;
|
|
1672
|
+
try {
|
|
1673
|
+
const res = await fetch('/api/preview/move', {
|
|
1674
|
+
method: 'POST',
|
|
1675
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1676
|
+
body: JSON.stringify({
|
|
1677
|
+
sourcePath,
|
|
1678
|
+
targetPath: targetDirPath,
|
|
1679
|
+
}),
|
|
1680
|
+
});
|
|
1681
|
+
const json = await res.json();
|
|
1682
|
+
if (json.ok) {
|
|
1683
|
+
if (json.noop) {
|
|
1684
|
+
showToast('目标路径与源路径相同', 'info');
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
renameOpenTabPaths(json.sourcePath, json.targetPath, json.isDir);
|
|
1688
|
+
setExpandedPaths(prev => new Set(prev).add(targetDirPath));
|
|
1689
|
+
await loadTree(true);
|
|
1690
|
+
showToast(`已移动 ${json.name} 到 ${getFileName(targetDirPath)}`, 'success');
|
|
1691
|
+
} else {
|
|
1692
|
+
showToast(json?.error?.message || '移动文件失败', 'error');
|
|
1693
|
+
}
|
|
1694
|
+
} catch (err) {
|
|
1695
|
+
showToast(`移动失败: ${err.message}`, 'error');
|
|
1696
|
+
}
|
|
1697
|
+
};
|
|
1698
|
+
|
|
1699
|
+
const handleDelete = async (node) => {
|
|
1700
|
+
if (!node) return;
|
|
1701
|
+
const isDir = node.isDir;
|
|
1702
|
+
const msg = `确定要永久删除${isDir ? '文件夹' : '文件'} "${node.name}" 吗?此操作不可恢复。`;
|
|
1703
|
+
if (typeof window !== 'undefined' && !window.confirm(msg)) return;
|
|
1704
|
+
|
|
1705
|
+
try {
|
|
1706
|
+
const res = await fetch('/api/preview/delete', {
|
|
1707
|
+
method: 'POST',
|
|
1708
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1709
|
+
body: JSON.stringify({ path: node.path }),
|
|
1710
|
+
});
|
|
1711
|
+
const json = await res.json();
|
|
1712
|
+
if (json.ok) {
|
|
1713
|
+
removeOpenTabsForPath(node.path, isDir);
|
|
1714
|
+
await loadTree(true);
|
|
1715
|
+
showToast(`已删除${isDir ? '文件夹' : '文件'}: ${node.name}`, 'info');
|
|
1716
|
+
} else {
|
|
1717
|
+
showToast(json?.error?.message || '删除失败', 'error');
|
|
1718
|
+
}
|
|
1719
|
+
} catch (err) {
|
|
1720
|
+
showToast(`删除失败: ${err.message}`, 'error');
|
|
1721
|
+
}
|
|
1722
|
+
};
|
|
1723
|
+
|
|
1724
|
+
const handleContextMenuAction = (action, node) => {
|
|
1725
|
+
const rootPath = treeData?.workspaceRoot || currentWs;
|
|
1726
|
+
if (action === 'new-file') {
|
|
1727
|
+
const target = node ? (node.isDir ? node.path : (node.path.substring(0, Math.max(node.path.lastIndexOf('/'), node.path.lastIndexOf('\\'))))) : rootPath;
|
|
1728
|
+
handleStartCreate(target, 'file');
|
|
1729
|
+
} else if (action === 'new-dir') {
|
|
1730
|
+
const target = node ? (node.isDir ? node.path : (node.path.substring(0, Math.max(node.path.lastIndexOf('/'), node.path.lastIndexOf('\\'))))) : rootPath;
|
|
1731
|
+
handleStartCreate(target, 'dir');
|
|
1732
|
+
} else if (action === 'preview') {
|
|
1733
|
+
if (node && !node.isDir) onOpenFile(node.path);
|
|
1734
|
+
} else if (action === 'cut') {
|
|
1735
|
+
if (node) {
|
|
1736
|
+
setClipboard({ action: 'cut', path: node.path, name: node.name, isDir: node.isDir });
|
|
1737
|
+
showToast(`已剪切: ${node.name}`, 'info');
|
|
1738
|
+
}
|
|
1739
|
+
} else if (action === 'paste') {
|
|
1740
|
+
if (clipboard && clipboard.action === 'cut') {
|
|
1741
|
+
const target = node ? (node.isDir ? node.path : (node.path.substring(0, Math.max(node.path.lastIndexOf('/'), node.path.lastIndexOf('\\'))))) : rootPath;
|
|
1742
|
+
handleMove(clipboard.path, target);
|
|
1743
|
+
setClipboard(null);
|
|
1744
|
+
}
|
|
1745
|
+
} else if (action === 'rename') {
|
|
1746
|
+
if (node) setRenamingPath(node.path);
|
|
1747
|
+
} else if (action === 'reveal') {
|
|
1748
|
+
if (node) triggerRevealInExplorer(node.path);
|
|
1749
|
+
} else if (action === 'native') {
|
|
1750
|
+
if (node) triggerOpenNative(node.path);
|
|
1751
|
+
} else if (action === 'delete') {
|
|
1752
|
+
if (node) handleDelete(node);
|
|
1753
|
+
} else if (action === 'refresh') {
|
|
1754
|
+
loadTree(true);
|
|
1755
|
+
}
|
|
1756
|
+
};
|
|
1757
|
+
|
|
1758
|
+
const rootPath = treeData?.workspaceRoot || currentWs;
|
|
1759
|
+
const isCreatingAtRoot = creatingItem && creatingItem.parentPath && rootPath && creatingItem.parentPath.replace(/\\/g, '/').toLowerCase() === rootPath.replace(/\\/g, '/').toLowerCase();
|
|
1760
|
+
const isRootDropTarget = dragOverNode && rootPath && dragOverNode.path && dragOverNode.path.replace(/\\/g, '/').toLowerCase() === rootPath.replace(/\\/g, '/').toLowerCase();
|
|
1761
|
+
|
|
1167
1762
|
return h('div', {
|
|
1168
1763
|
className: 'dsh-tree-sidebar',
|
|
1169
1764
|
style: { width: `${width}px` },
|
|
@@ -1186,6 +1781,18 @@
|
|
|
1186
1781
|
onClick: () => loadTree(true),
|
|
1187
1782
|
title: '刷新文件树',
|
|
1188
1783
|
}, '🔄'),
|
|
1784
|
+
h('button', {
|
|
1785
|
+
type: 'button',
|
|
1786
|
+
className: 'dsh-tree-action-btn',
|
|
1787
|
+
onClick: () => handleStartCreate(rootPath, 'file'),
|
|
1788
|
+
title: '在根目录新建文件',
|
|
1789
|
+
}, '📄➕'),
|
|
1790
|
+
h('button', {
|
|
1791
|
+
type: 'button',
|
|
1792
|
+
className: 'dsh-tree-action-btn',
|
|
1793
|
+
onClick: () => handleStartCreate(rootPath, 'dir'),
|
|
1794
|
+
title: '在根目录新建文件夹',
|
|
1795
|
+
}, '📁➕'),
|
|
1189
1796
|
h('button', {
|
|
1190
1797
|
type: 'button',
|
|
1191
1798
|
className: 'dsh-tree-action-btn',
|
|
@@ -1283,7 +1890,40 @@
|
|
|
1283
1890
|
),
|
|
1284
1891
|
|
|
1285
1892
|
// Tree Content
|
|
1286
|
-
h('div', {
|
|
1893
|
+
h('div', {
|
|
1894
|
+
className: `dsh-tree-node-list ${isRootDropTarget ? 'drop-target-root' : ''}`,
|
|
1895
|
+
onContextMenu: (e) => {
|
|
1896
|
+
e.preventDefault();
|
|
1897
|
+
setContextMenu({ x: e.clientX, y: e.clientY, node: null, isRoot: true });
|
|
1898
|
+
},
|
|
1899
|
+
onDragOver: (e) => {
|
|
1900
|
+
if (draggedNode && isValidDropTarget(draggedNode, { path: rootPath, isDir: true })) {
|
|
1901
|
+
e.preventDefault();
|
|
1902
|
+
e.dataTransfer.dropEffect = 'move';
|
|
1903
|
+
setDragOverNode({ path: rootPath, isDir: true });
|
|
1904
|
+
}
|
|
1905
|
+
},
|
|
1906
|
+
onDragLeave: () => {
|
|
1907
|
+
if (dragOverNode && dragOverNode.path === rootPath) {
|
|
1908
|
+
setDragOverNode(null);
|
|
1909
|
+
}
|
|
1910
|
+
},
|
|
1911
|
+
onDrop: (e) => {
|
|
1912
|
+
e.preventDefault();
|
|
1913
|
+
setDragOverNode(null);
|
|
1914
|
+
if (draggedNode && isValidDropTarget(draggedNode, { path: rootPath, isDir: true })) {
|
|
1915
|
+
handleMove(draggedNode.path, rootPath);
|
|
1916
|
+
}
|
|
1917
|
+
},
|
|
1918
|
+
},
|
|
1919
|
+
// If creating at root level, render inline create input at top
|
|
1920
|
+
isCreatingAtRoot ? h(InlineCreateInputNode, {
|
|
1921
|
+
depth: 0,
|
|
1922
|
+
type: creatingItem.type,
|
|
1923
|
+
onConfirm: handleConfirmCreate,
|
|
1924
|
+
onCancel: () => setCreatingItem(null),
|
|
1925
|
+
}) : null,
|
|
1926
|
+
|
|
1287
1927
|
isLoading ? (
|
|
1288
1928
|
h('div', { className: 'dsh-tree-loading' }, '正在扫描工作区文件...')
|
|
1289
1929
|
) : error ? (
|
|
@@ -1295,7 +1935,7 @@
|
|
|
1295
1935
|
onClick: () => loadTree(true),
|
|
1296
1936
|
}, '重试')
|
|
1297
1937
|
)
|
|
1298
|
-
) : displayTree.length === 0 ? (
|
|
1938
|
+
) : displayTree.length === 0 && !isCreatingAtRoot ? (
|
|
1299
1939
|
h('div', { className: 'dsh-tree-empty' }, searchQuery ? '未匹配到相关文件' : '工作区暂无文件')
|
|
1300
1940
|
) : (
|
|
1301
1941
|
displayTree.map(node => h(FileTreeNodeItem, {
|
|
@@ -1307,10 +1947,42 @@
|
|
|
1307
1947
|
activeFilePath,
|
|
1308
1948
|
onOpenFile,
|
|
1309
1949
|
searchQuery,
|
|
1950
|
+
creatingItem,
|
|
1951
|
+
onConfirmCreate: handleConfirmCreate,
|
|
1952
|
+
onCancelCreate: () => setCreatingItem(null),
|
|
1953
|
+
renamingPath,
|
|
1954
|
+
onConfirmRename: handleConfirmRename,
|
|
1955
|
+
onCancelRename: () => setRenamingPath(null),
|
|
1956
|
+
onContextMenu: (targetNode, e) => {
|
|
1957
|
+
setContextMenu({ x: e.clientX, y: e.clientY, node: targetNode, isRoot: false });
|
|
1958
|
+
},
|
|
1959
|
+
draggedNode,
|
|
1960
|
+
dragOverNode,
|
|
1961
|
+
onDragStart: (targetNode) => setDraggedNode(targetNode),
|
|
1962
|
+
onDragEnd: () => { setDraggedNode(null); setDragOverNode(null); },
|
|
1963
|
+
onDragOver: (targetNode) => setDragOverNode(targetNode),
|
|
1964
|
+
onDragLeave: () => setDragOverNode(null),
|
|
1965
|
+
onDrop: (source, target) => {
|
|
1966
|
+
setDraggedNode(null);
|
|
1967
|
+
setDragOverNode(null);
|
|
1968
|
+
handleMove(source.path, target.path);
|
|
1969
|
+
},
|
|
1970
|
+
clipboard,
|
|
1310
1971
|
}))
|
|
1311
1972
|
)
|
|
1312
1973
|
),
|
|
1313
1974
|
|
|
1975
|
+
// Context Menu Overlay
|
|
1976
|
+
contextMenu ? h(TreeContextMenu, {
|
|
1977
|
+
x: contextMenu.x,
|
|
1978
|
+
y: contextMenu.y,
|
|
1979
|
+
node: contextMenu.node,
|
|
1980
|
+
isRoot: contextMenu.isRoot,
|
|
1981
|
+
clipboard,
|
|
1982
|
+
onAction: handleContextMenuAction,
|
|
1983
|
+
onClose: () => setContextMenu(null),
|
|
1984
|
+
}) : null,
|
|
1985
|
+
|
|
1314
1986
|
// Sidebar Footer with file count stats
|
|
1315
1987
|
treeData ? h('div', { className: 'dsh-tree-sidebar-footer' },
|
|
1316
1988
|
h('span', null, `${treeData.totalDirs || 0} 目录`),
|
|
@@ -2242,22 +2914,62 @@
|
|
|
2242
2914
|
}
|
|
2243
2915
|
|
|
2244
2916
|
// =========================================================================
|
|
2245
|
-
// React Component: CodeEditorView
|
|
2917
|
+
// React Component: CodeEditorView (Unified line-row architecture)
|
|
2246
2918
|
// =========================================================================
|
|
2247
|
-
function
|
|
2248
|
-
|
|
2919
|
+
function splitHighlightedHtmlIntoLines(html) {
|
|
2920
|
+
if (!html) return [];
|
|
2921
|
+
const rawLines = html.split('\n');
|
|
2922
|
+
const lines = [];
|
|
2923
|
+
const openTags = [];
|
|
2924
|
+
|
|
2925
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
2926
|
+
let line = rawLines[i];
|
|
2927
|
+
let prefix = '';
|
|
2928
|
+
if (openTags.length > 0) {
|
|
2929
|
+
prefix = openTags.map(t => '<' + t.name + (t.attrs ? ' ' + t.attrs : '') + '>').join('');
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
const tagRegex = /<\/?([a-zA-Z0-9\-]+)([^>]*)>/g;
|
|
2933
|
+
let match;
|
|
2934
|
+
while ((match = tagRegex.exec(line)) !== null) {
|
|
2935
|
+
const isClosing = match[0].startsWith('</');
|
|
2936
|
+
const tagName = match[1];
|
|
2937
|
+
const attrs = match[2];
|
|
2938
|
+
if (isClosing) {
|
|
2939
|
+
const lastIdx = openTags.map(t => t.name).lastIndexOf(tagName);
|
|
2940
|
+
if (lastIdx !== -1) {
|
|
2941
|
+
openTags.splice(lastIdx, 1);
|
|
2942
|
+
}
|
|
2943
|
+
} else if (!match[0].endsWith('/>')) {
|
|
2944
|
+
openTags.push({ name: tagName, attrs: attrs.trim() });
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2249
2947
|
|
|
2250
|
-
|
|
2948
|
+
let suffix = '';
|
|
2949
|
+
if (openTags.length > 0) {
|
|
2950
|
+
suffix = openTags.slice().reverse().map(t => '</' + t.name + '>').join('');
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
lines.push(prefix + line + suffix);
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
return lines;
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
function CodeEditorView({ content, category, isHighlighted = true, isWordWrap = true, searchQuery = '', onLineClick }) {
|
|
2960
|
+
const normalizedContent = useMemo(() => {
|
|
2251
2961
|
if (!content) return '';
|
|
2962
|
+
return content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
2963
|
+
}, [content]);
|
|
2964
|
+
|
|
2965
|
+
const highlightedLines = useMemo(() => {
|
|
2966
|
+
if (!normalizedContent) return [];
|
|
2967
|
+
let html = '';
|
|
2252
2968
|
if (!isHighlighted) {
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
escaped = escaped.replace(new RegExp(`(${q})`, 'gi'), '<mark class="dsh-search-highlight">$1</mark>');
|
|
2257
|
-
}
|
|
2258
|
-
return escaped;
|
|
2969
|
+
html = escapeHtml(normalizedContent);
|
|
2970
|
+
} else {
|
|
2971
|
+
html = highlightCode(normalizedContent, category);
|
|
2259
2972
|
}
|
|
2260
|
-
let html = highlightCode(content, category);
|
|
2261
2973
|
if (searchQuery.trim()) {
|
|
2262
2974
|
try {
|
|
2263
2975
|
const q = escapeHtml(searchQuery.trim());
|
|
@@ -2265,27 +2977,34 @@
|
|
|
2265
2977
|
html = html.replace(regex, '<mark class="dsh-search-highlight">$1</mark>');
|
|
2266
2978
|
} catch (e) {}
|
|
2267
2979
|
}
|
|
2268
|
-
return html;
|
|
2269
|
-
}, [
|
|
2980
|
+
return splitHighlightedHtmlIntoLines(html);
|
|
2981
|
+
}, [normalizedContent, category, isHighlighted, searchQuery]);
|
|
2982
|
+
|
|
2983
|
+
const lineCount = highlightedLines.length;
|
|
2984
|
+
const digitCount = Math.max(2, String(lineCount).length);
|
|
2985
|
+
const gutterWidth = Math.max(24, digitCount * 7.5 + 12);
|
|
2270
2986
|
|
|
2271
2987
|
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
|
-
|
|
2988
|
+
highlightedLines.map((lineHtml, i) => {
|
|
2989
|
+
const lineNum = i + 1;
|
|
2990
|
+
return h('div', {
|
|
2991
|
+
key: lineNum,
|
|
2992
|
+
id: `line-row-${lineNum}`,
|
|
2993
|
+
className: 'dsh-code-line',
|
|
2994
|
+
},
|
|
2995
|
+
h('span', {
|
|
2996
|
+
id: `line-gutter-${lineNum}`,
|
|
2997
|
+
className: 'dsh-line-gutter',
|
|
2998
|
+
style: { minWidth: `${gutterWidth}px` },
|
|
2999
|
+
onClick: () => onLineClick && onLineClick(lineNum),
|
|
3000
|
+
title: `第 ${lineNum} 行 (点击定位)`,
|
|
3001
|
+
}, lineNum),
|
|
3002
|
+
h('span', {
|
|
3003
|
+
className: 'dsh-line-code',
|
|
3004
|
+
dangerouslySetInnerHTML: { __html: lineHtml ? lineHtml : ' ' },
|
|
2286
3005
|
})
|
|
2287
|
-
)
|
|
2288
|
-
)
|
|
3006
|
+
);
|
|
3007
|
+
})
|
|
2289
3008
|
);
|
|
2290
3009
|
}
|
|
2291
3010
|
|
|
@@ -2387,7 +3106,9 @@
|
|
|
2387
3106
|
const el = contentRef.current.querySelector(`#${item.id}`);
|
|
2388
3107
|
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
2389
3108
|
} else {
|
|
2390
|
-
const
|
|
3109
|
+
const rowEl = document.getElementById(`line-row-${item.line}`);
|
|
3110
|
+
const gutterEl = document.getElementById(`line-gutter-${item.line}`);
|
|
3111
|
+
const el = rowEl || gutterEl;
|
|
2391
3112
|
if (el) {
|
|
2392
3113
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
2393
3114
|
el.classList.add('dsh-line-highlight-flash');
|
|
@@ -4741,6 +5462,124 @@
|
|
|
4741
5462
|
}
|
|
4742
5463
|
.dsh-tree-footer-dot { font-size: 8px; }
|
|
4743
5464
|
|
|
5465
|
+
/* Drag & Drop & Cut Styles */
|
|
5466
|
+
.dsh-tree-node-row.drop-target {
|
|
5467
|
+
background: rgba(14, 165, 233, 0.22) !important;
|
|
5468
|
+
outline: 1px dashed #0284c7 !important;
|
|
5469
|
+
color: #0284c7 !important;
|
|
5470
|
+
}
|
|
5471
|
+
body[data-ds-dark-theme] .dsh-tree-node-row.drop-target {
|
|
5472
|
+
background: rgba(56, 189, 248, 0.25) !important;
|
|
5473
|
+
outline-color: #38bdf8 !important;
|
|
5474
|
+
color: #38bdf8 !important;
|
|
5475
|
+
}
|
|
5476
|
+
.dsh-tree-node-list.drop-target-root {
|
|
5477
|
+
background: rgba(14, 165, 233, 0.08) !important;
|
|
5478
|
+
outline: 2px dashed #0284c7 !important;
|
|
5479
|
+
outline-offset: -4px;
|
|
5480
|
+
}
|
|
5481
|
+
body[data-ds-dark-theme] .dsh-tree-node-list.drop-target-root {
|
|
5482
|
+
background: rgba(56, 189, 248, 0.1) !important;
|
|
5483
|
+
outline-color: #38bdf8 !important;
|
|
5484
|
+
}
|
|
5485
|
+
.dsh-tree-node-row.is-dragged {
|
|
5486
|
+
opacity: 0.45;
|
|
5487
|
+
filter: grayscale(0.6);
|
|
5488
|
+
}
|
|
5489
|
+
.dsh-tree-node-row.is-cut {
|
|
5490
|
+
opacity: 0.5;
|
|
5491
|
+
filter: grayscale(0.5);
|
|
5492
|
+
border: 1px dashed #94a3b8;
|
|
5493
|
+
}
|
|
5494
|
+
|
|
5495
|
+
/* Inline Creation & Rename Input */
|
|
5496
|
+
.dsh-tree-inline-input-row {
|
|
5497
|
+
display: flex;
|
|
5498
|
+
align-items: center;
|
|
5499
|
+
height: 24px;
|
|
5500
|
+
margin: 1px 4px;
|
|
5501
|
+
gap: 3px;
|
|
5502
|
+
box-sizing: border-box;
|
|
5503
|
+
}
|
|
5504
|
+
.dsh-tree-inline-input {
|
|
5505
|
+
flex: 1;
|
|
5506
|
+
height: 20px;
|
|
5507
|
+
border-radius: 3px;
|
|
5508
|
+
border: 1px solid #38bdf8;
|
|
5509
|
+
background: var(--dsw-alias-bg-base, #ffffff);
|
|
5510
|
+
color: inherit;
|
|
5511
|
+
font-size: 11px;
|
|
5512
|
+
padding: 0 4px;
|
|
5513
|
+
outline: none;
|
|
5514
|
+
box-sizing: border-box;
|
|
5515
|
+
box-shadow: 0 0 0 1px rgba(56, 189, 248, 0.4);
|
|
5516
|
+
}
|
|
5517
|
+
body[data-ds-dark-theme] .dsh-tree-inline-input {
|
|
5518
|
+
background: #1e2330;
|
|
5519
|
+
color: #f1f5f9;
|
|
5520
|
+
border-color: #38bdf8;
|
|
5521
|
+
}
|
|
5522
|
+
|
|
5523
|
+
/* Tree Context Menu */
|
|
5524
|
+
.dsh-tree-context-menu {
|
|
5525
|
+
position: fixed;
|
|
5526
|
+
min-width: 170px;
|
|
5527
|
+
background: var(--dsw-alias-bg-layer-1, #ffffff);
|
|
5528
|
+
border: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
|
|
5529
|
+
border-radius: 6px;
|
|
5530
|
+
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.2), 0 8px 10px -6px rgba(0, 0, 0, 0.15);
|
|
5531
|
+
padding: 4px;
|
|
5532
|
+
user-select: none;
|
|
5533
|
+
font-family: inherit;
|
|
5534
|
+
}
|
|
5535
|
+
body[data-ds-dark-theme] .dsh-tree-context-menu {
|
|
5536
|
+
background: #1a1e29;
|
|
5537
|
+
border-color: #2e384d;
|
|
5538
|
+
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5);
|
|
5539
|
+
}
|
|
5540
|
+
.dsh-menu-item {
|
|
5541
|
+
display: flex;
|
|
5542
|
+
align-items: center;
|
|
5543
|
+
gap: 8px;
|
|
5544
|
+
padding: 5px 8px;
|
|
5545
|
+
border-radius: 4px;
|
|
5546
|
+
font-size: 12px;
|
|
5547
|
+
cursor: pointer;
|
|
5548
|
+
color: var(--dsw-alias-label-primary, #1e293b);
|
|
5549
|
+
transition: background 0.1s;
|
|
5550
|
+
}
|
|
5551
|
+
body[data-ds-dark-theme] .dsh-menu-item {
|
|
5552
|
+
color: #e2e8f0;
|
|
5553
|
+
}
|
|
5554
|
+
.dsh-menu-item:hover {
|
|
5555
|
+
background: var(--dsw-alias-bg-hover, rgba(0, 0, 0, 0.06));
|
|
5556
|
+
}
|
|
5557
|
+
body[data-ds-dark-theme] .dsh-menu-item:hover {
|
|
5558
|
+
background: rgba(255, 255, 255, 0.09);
|
|
5559
|
+
}
|
|
5560
|
+
.dsh-menu-item-danger {
|
|
5561
|
+
color: #ef4444 !important;
|
|
5562
|
+
}
|
|
5563
|
+
.dsh-menu-item-danger:hover {
|
|
5564
|
+
background: rgba(239, 68, 68, 0.12) !important;
|
|
5565
|
+
}
|
|
5566
|
+
.dsh-menu-icon {
|
|
5567
|
+
font-size: 13px;
|
|
5568
|
+
width: 16px;
|
|
5569
|
+
display: inline-flex;
|
|
5570
|
+
align-items: center;
|
|
5571
|
+
justify-content: center;
|
|
5572
|
+
flex-shrink: 0;
|
|
5573
|
+
}
|
|
5574
|
+
.dsh-menu-divider {
|
|
5575
|
+
height: 1px;
|
|
5576
|
+
background: var(--dsw-alias-border-l3, #e2e8f0);
|
|
5577
|
+
margin: 3px 4px;
|
|
5578
|
+
}
|
|
5579
|
+
body[data-ds-dark-theme] .dsh-menu-divider {
|
|
5580
|
+
background: #2a3140;
|
|
5581
|
+
}
|
|
5582
|
+
|
|
4744
5583
|
/* Splitter between Tree and Preview */
|
|
4745
5584
|
.dsh-tree-splitter {
|
|
4746
5585
|
width: 4px;
|
|
@@ -4860,85 +5699,89 @@
|
|
|
4860
5699
|
body[data-ds-dark-theme] .tok-prop { color: #9cdcfe; }
|
|
4861
5700
|
body[data-ds-dark-theme] .tok-selector { color: #d7ba7d; }
|
|
4862
5701
|
|
|
4863
|
-
/* Code Editor Viewer */
|
|
5702
|
+
/* Code Editor Viewer (with unified line row architecture for 100% perfect alignment) */
|
|
4864
5703
|
.dsh-code-editor-view {
|
|
4865
5704
|
display: flex;
|
|
5705
|
+
flex-direction: column;
|
|
5706
|
+
width: 100%;
|
|
4866
5707
|
min-height: 100%;
|
|
4867
|
-
|
|
4868
|
-
font-
|
|
5708
|
+
overflow: auto;
|
|
5709
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Cascadia Code", "Fira Code", monospace;
|
|
5710
|
+
font-size: 12px;
|
|
4869
5711
|
line-height: 20px;
|
|
4870
5712
|
background: var(--dsw-alias-bg-layer-1, #ffffff);
|
|
4871
5713
|
color: var(--dsw-alias-label-primary, #1e293b);
|
|
5714
|
+
padding: 8px 0;
|
|
5715
|
+
box-sizing: border-box;
|
|
5716
|
+
user-select: text;
|
|
4872
5717
|
}
|
|
4873
5718
|
body[data-ds-dark-theme] .dsh-code-editor-view {
|
|
4874
|
-
background: #
|
|
5719
|
+
background: #161922;
|
|
4875
5720
|
color: #d4d4d4;
|
|
4876
5721
|
}
|
|
4877
5722
|
|
|
4878
|
-
.dsh-code-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
5723
|
+
.dsh-code-line {
|
|
5724
|
+
display: flex;
|
|
5725
|
+
flex-direction: row;
|
|
5726
|
+
align-items: flex-start;
|
|
5727
|
+
min-height: 20px;
|
|
5728
|
+
line-height: 20px;
|
|
5729
|
+
width: 100%;
|
|
5730
|
+
box-sizing: border-box;
|
|
5731
|
+
transition: background 0.1s;
|
|
5732
|
+
}
|
|
5733
|
+
.dsh-code-line:hover {
|
|
5734
|
+
background: rgba(0, 0, 0, 0.035);
|
|
5735
|
+
}
|
|
5736
|
+
body[data-ds-dark-theme] .dsh-code-line:hover {
|
|
5737
|
+
background: rgba(255, 255, 255, 0.035);
|
|
5738
|
+
}
|
|
5739
|
+
.dsh-code-line.dsh-line-highlight-flash {
|
|
5740
|
+
background: rgba(14, 165, 233, 0.22) !important;
|
|
5741
|
+
}
|
|
5742
|
+
|
|
5743
|
+
.dsh-line-gutter {
|
|
5744
|
+
display: inline-block;
|
|
5745
|
+
box-sizing: border-box;
|
|
5746
|
+
padding: 0 8px 0 6px;
|
|
4882
5747
|
text-align: right;
|
|
4883
5748
|
user-select: none;
|
|
4884
|
-
background: var(--dsw-alias-bg-layer-2, #f8fafc);
|
|
4885
|
-
border-right: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
|
|
4886
5749
|
color: var(--dsw-alias-label-tertiary, #94a3b8);
|
|
4887
|
-
font-size:
|
|
5750
|
+
font-size: 11px;
|
|
4888
5751
|
line-height: 20px;
|
|
4889
5752
|
flex-shrink: 0;
|
|
5753
|
+
border-right: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
|
|
5754
|
+
cursor: pointer;
|
|
5755
|
+
transition: color 0.1s;
|
|
4890
5756
|
}
|
|
4891
|
-
body[data-ds-dark-theme] .dsh-
|
|
4892
|
-
background: #141720;
|
|
5757
|
+
body[data-ds-dark-theme] .dsh-line-gutter {
|
|
4893
5758
|
border-right-color: #2a3140;
|
|
4894
|
-
color: #
|
|
4895
|
-
}
|
|
4896
|
-
|
|
4897
|
-
.dsh-gutter-line {
|
|
4898
|
-
height: 20px;
|
|
4899
|
-
cursor: pointer;
|
|
4900
|
-
padding-right: 8px;
|
|
5759
|
+
color: #55637a;
|
|
4901
5760
|
}
|
|
4902
|
-
.dsh-gutter
|
|
5761
|
+
.dsh-line-gutter:hover {
|
|
4903
5762
|
color: var(--dsw-brand-primary, #0284c7);
|
|
4904
|
-
font-weight:
|
|
5763
|
+
font-weight: 600;
|
|
4905
5764
|
}
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
color: #0284c7;
|
|
5765
|
+
body[data-ds-dark-theme] .dsh-line-gutter:hover {
|
|
5766
|
+
color: #38bdf8;
|
|
4909
5767
|
}
|
|
4910
5768
|
|
|
4911
|
-
.dsh-code
|
|
5769
|
+
.dsh-line-code {
|
|
4912
5770
|
flex: 1;
|
|
4913
5771
|
min-width: 0;
|
|
4914
|
-
padding: 12px
|
|
4915
|
-
|
|
4916
|
-
}
|
|
4917
|
-
.dsh-code-pre {
|
|
4918
|
-
margin: 0;
|
|
5772
|
+
padding: 0 12px 0 10px;
|
|
5773
|
+
white-space: pre;
|
|
4919
5774
|
font-family: inherit;
|
|
4920
5775
|
font-size: inherit;
|
|
4921
5776
|
line-height: 20px;
|
|
4922
5777
|
tab-size: 2;
|
|
4923
|
-
|
|
5778
|
+
box-sizing: border-box;
|
|
4924
5779
|
}
|
|
4925
|
-
.dsh-code-editor-view.word-wrap .dsh-code
|
|
5780
|
+
.dsh-code-editor-view.word-wrap .dsh-line-code {
|
|
4926
5781
|
white-space: pre-wrap;
|
|
4927
5782
|
word-break: normal;
|
|
4928
5783
|
overflow-wrap: break-word;
|
|
4929
5784
|
}
|
|
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
5785
|
|
|
4943
5786
|
/* Interactive JSON Inspector */
|
|
4944
5787
|
.dsh-json-inspector {
|