dsh-plugin-workbench 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -30,7 +30,7 @@ window.__ModuleLoader__.load({
30
30
  let react = require("react");
31
31
  let react_jsx_runtime = require("react/jsx-runtime");
32
32
  //#region \0dsh-css:src/client/files.module.css.mjs
33
- const css$1 = "[data-fe-theme=light]{--fe-bg:#fff;--fe-sidebar-bg:#f3f3f3;--fe-fg:#1f1f1f;--fe-muted:#6e6e6e;--fe-faint:#9d9d9d;--fe-border:#e5e5e5;--fe-border-strong:#c8c8c8;--fe-accent:#005fb8;--fe-hover:#e8e8e8;--fe-selection:#add6ff;--fe-token-keyword:#00f;--fe-token-string:#a31515;--fe-token-comment:green;--fe-token-number:#098658;--fe-token-function:#795e26;--fe-token-type:#267f99;--fe-token-variable:#001080;--fe-token-tag:maroon;--fe-token-meta:#6e6e6e}[data-fe-theme=dark]{--fe-bg:#1e1e1e;--fe-sidebar-bg:#252526;--fe-fg:#d4d4d4;--fe-muted:#ccc;--fe-faint:#9d9d9d;--fe-border:#3c3c3c;--fe-border-strong:#454545;--fe-accent:#007acc;--fe-hover:#2a2d2e;--fe-selection:#264f78;--fe-token-keyword:#569cd6;--fe-token-string:#ce9178;--fe-token-comment:#6a9955;--fe-token-number:#b5cea8;--fe-token-function:#dcdcaa;--fe-token-type:#4ec9b0;--fe-token-variable:#9cdcfe;--fe-token-tag:#569cd6;--fe-token-meta:#9b9b9b}.Myjg4a_column{box-sizing:border-box;background:var(--fe-sidebar-bg);min-width:0;height:100%;color:var(--fe-fg);flex-direction:column;font-size:13px;display:flex;overflow:hidden}.Myjg4a_header{box-sizing:border-box;border-bottom:1px solid var(--fe-border);flex:none;align-items:center;gap:6px;height:34px;padding:0 8px;display:flex}.Myjg4a_headerTitle{text-overflow:ellipsis;white-space:nowrap;text-transform:uppercase;letter-spacing:.04em;min-width:0;color:var(--fe-muted);flex:1;font-size:11px;font-weight:600;overflow:hidden}.Myjg4a_headerActions{flex:none;align-items:center;gap:2px;display:flex}.Myjg4a_action{appearance:none;color:var(--fe-muted);cursor:pointer;background:0 0;border:none;border-radius:5px;justify-content:center;align-items:center;width:22px;height:22px;padding:0;font-size:13px;line-height:1;display:inline-flex}.Myjg4a_action:hover:not(:disabled){background:var(--fe-hover);color:var(--fe-fg)}.Myjg4a_action:disabled{opacity:.5;cursor:default}.Myjg4a_treeArea{flex:1;min-height:0;padding:4px;overflow-y:auto}.Myjg4a_row{cursor:pointer;user-select:none;white-space:nowrap;height:22px;color:var(--fe-fg);border-radius:3px;align-items:center;gap:6px;font-size:13px;display:flex}.Myjg4a_row:hover{background:var(--fe-hover)}.Myjg4a_rowSelected{background:var(--fe-selection)}.Myjg4a_rowSelected .Myjg4a_name{color:var(--fe-fg)}.Myjg4a_chevron{text-align:center;width:12px;color:var(--fe-muted);flex:none;font-size:10px;line-height:22px}.Myjg4a_icon{text-align:center;flex:none;width:16px;font-size:13px;line-height:22px}.Myjg4a_name{text-overflow:ellipsis;flex:1;min-width:0;overflow:hidden}.Myjg4a_size{color:var(--fe-faint);flex:none;font-size:11px}.Myjg4a_actions{opacity:0;flex:none;align-items:center;gap:2px;transition:opacity .12s;display:flex}.Myjg4a_row:hover .Myjg4a_actions,.Myjg4a_row:focus-within .Myjg4a_actions{opacity:1}.Myjg4a_rowHint{color:var(--fe-faint);padding:2px 8px 2px 0;font-size:12px;line-height:20px}.Myjg4a_rowError{color:#f48771;padding:2px 8px 2px 0;font-size:12px;line-height:18px}.Myjg4a_preview{border-right:1px solid var(--fe-border);background:var(--fe-bg);flex-direction:column;flex:0 0 55%;min-width:240px;min-height:0;animation:.18s Myjg4a_fe-preview-in;display:flex;overflow:hidden}.Myjg4a_previewClosing{animation:.2s forwards Myjg4a_fe-preview-out}@keyframes Myjg4a_fe-preview-in{0%{opacity:0}to{opacity:1}}@keyframes Myjg4a_fe-preview-out{0%{opacity:1}to{opacity:0}}.Myjg4a_handle{cursor:col-resize;touch-action:none;z-index:2;background:0 0;flex:none;width:6px;margin-left:-4px;margin-right:-2px}.Myjg4a_handle:hover,.Myjg4a_handle:active{background:var(--fe-accent);opacity:.35}.Myjg4a_tabBar{border-bottom:1px solid var(--fe-border);background:var(--fe-sidebar-bg);flex:none;align-items:stretch;height:34px;display:flex}.Myjg4a_tabScroller{scrollbar-width:thin;flex:1;align-items:stretch;min-width:0;display:flex;overflow-x:auto}.Myjg4a_collapse{appearance:none;color:var(--fe-muted);cursor:pointer;background:0 0;border:none;border-radius:4px;flex:none;justify-content:center;align-items:center;width:26px;height:26px;margin:auto 4px;padding:0;font-size:14px;line-height:1;display:inline-flex}.Myjg4a_collapse:hover{background:var(--fe-hover);color:var(--fe-fg)}.Myjg4a_tab{max-width:180px;color:var(--fe-muted);border-right:1px solid var(--fe-border);cursor:pointer;user-select:none;white-space:nowrap;flex:none;align-items:center;gap:6px;padding:0 8px 0 12px;font-size:13px;display:flex}.Myjg4a_tab:hover{background:var(--fe-hover)}.Myjg4a_tabActive{background:var(--fe-bg);color:var(--fe-fg);box-shadow:inset 0 -1px 0 var(--fe-accent)}.Myjg4a_tabName{text-overflow:ellipsis;flex:1;min-width:0;overflow:hidden}.Myjg4a_tabClose{appearance:none;color:var(--fe-faint);cursor:pointer;background:0 0;border:none;border-radius:3px;justify-content:center;align-items:center;width:16px;height:16px;padding:0;font-size:11px;display:inline-flex}.Myjg4a_tabClose:hover{background:var(--fe-hover);color:var(--fe-fg)}.Myjg4a_tabDot{background:var(--fe-fg);opacity:.85;border-radius:50%;flex:none;width:8px;height:8px}.Myjg4a_previewBody{flex:1;min-height:0;overflow:auto}.Myjg4a_editor{height:100%;min-height:0;position:relative}.Myjg4a_editorHighlight{pointer-events:none;white-space:pre;color:var(--fe-fg);background:0 0;margin:0;padding:12px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;line-height:1.5;position:absolute;inset:0;overflow:hidden}.Myjg4a_editorHighlight code{font-family:inherit;font-size:inherit;line-height:inherit;white-space:inherit}.Myjg4a_editorTextarea{box-sizing:border-box;resize:none;color:#0000;caret-color:var(--fe-fg);white-space:pre;tab-size:2;background:0 0;border:none;outline:none;margin:0;padding:12px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;line-height:1.5;position:absolute;inset:0;overflow:auto}.Myjg4a_saveError{color:#1e1e1e;background:#f48771;border-radius:4px;padding:4px 10px;font-size:12px;position:absolute;bottom:8px;right:8px}.Myjg4a_previewHint{color:var(--fe-faint);padding:12px;font-size:12px;line-height:18px}.Myjg4a_previewMeta{color:var(--fe-faint);font-size:11px}.Myjg4a_placeholder{text-align:center;color:var(--fe-faint);flex:1;justify-content:center;align-items:center;padding:16px;font-size:12px;line-height:18px;display:flex}.Myjg4a_editorHighlight .hljs-keyword,.Myjg4a_editorHighlight .hljs-selector-tag,.Myjg4a_editorHighlight .hljs-literal{color:var(--fe-token-keyword)}.Myjg4a_editorHighlight .hljs-string,.Myjg4a_editorHighlight .hljs-regexp,.Myjg4a_editorHighlight .hljs-addition{color:var(--fe-token-string)}.Myjg4a_editorHighlight .hljs-comment,.Myjg4a_editorHighlight .hljs-quote{color:var(--fe-token-comment);font-style:italic}.Myjg4a_editorHighlight .hljs-number,.Myjg4a_editorHighlight .hljs-symbol,.Myjg4a_editorHighlight .hljs-bullet{color:var(--fe-token-number)}.Myjg4a_editorHighlight .hljs-title,.Myjg4a_editorHighlight .hljs-section,.Myjg4a_editorHighlight .hljs-title.function_{color:var(--fe-token-function)}.Myjg4a_editorHighlight .hljs-type,.Myjg4a_editorHighlight .hljs-built_in,.Myjg4a_editorHighlight .hljs-class .hljs-title{color:var(--fe-token-type)}.Myjg4a_editorHighlight .hljs-attr,.Myjg4a_editorHighlight .hljs-attribute,.Myjg4a_editorHighlight .hljs-variable,.Myjg4a_editorHighlight .hljs-template-variable,.Myjg4a_editorHighlight .hljs-params{color:var(--fe-token-variable)}.Myjg4a_editorHighlight .hljs-tag,.Myjg4a_editorHighlight .hljs-name{color:var(--fe-token-tag)}.Myjg4a_editorHighlight .hljs-meta{color:var(--fe-token-meta)}.Myjg4a_editorHighlight .hljs-emphasis{font-style:italic}.Myjg4a_editorHighlight .hljs-strong{font-weight:700}body[data-dsh-maid-atelier] [data-skin-chrome=top-trim],body[data-dsh-maid-atelier] [data-skin-chrome=bottom-trim]{translate:calc(var(--maid-sidebar-width,0px) + var(--dsh-explorer-width,0px) + var(--dsh-preview-width,0px)) 0!important}[class*=explorerCol]{border-right:1px solid var(--fe-border)!important}";
33
+ const css$1 = "[data-fe-theme=light]{--fe-bg:#fff;--fe-sidebar-bg:#f3f3f3;--fe-fg:#1f1f1f;--fe-muted:#6e6e6e;--fe-faint:#9d9d9d;--fe-border:#e5e5e5;--fe-border-strong:#c8c8c8;--fe-accent:#005fb8;--fe-hover:#e8e8e8;--fe-selection:#add6ff;--fe-token-keyword:#00f;--fe-token-string:#a31515;--fe-token-comment:green;--fe-token-number:#098658;--fe-token-function:#795e26;--fe-token-type:#267f99;--fe-token-variable:#001080;--fe-token-tag:maroon;--fe-token-meta:#6e6e6e}[data-fe-theme=dark]{--fe-bg:#1e1e1e;--fe-sidebar-bg:#252526;--fe-fg:#d4d4d4;--fe-muted:#ccc;--fe-faint:#9d9d9d;--fe-border:#3c3c3c;--fe-border-strong:#454545;--fe-accent:#007acc;--fe-hover:#2a2d2e;--fe-selection:#264f78;--fe-token-keyword:#569cd6;--fe-token-string:#ce9178;--fe-token-comment:#6a9955;--fe-token-number:#b5cea8;--fe-token-function:#dcdcaa;--fe-token-type:#4ec9b0;--fe-token-variable:#9cdcfe;--fe-token-tag:#569cd6;--fe-token-meta:#9b9b9b}.Myjg4a_column{box-sizing:border-box;background:var(--fe-sidebar-bg);min-width:0;height:100%;color:var(--fe-fg);flex-direction:column;font-size:13px;display:flex;overflow:hidden}.Myjg4a_header{box-sizing:border-box;border-bottom:1px solid var(--fe-border);flex:none;align-items:center;gap:6px;height:34px;padding:0 8px;display:flex}.Myjg4a_headerTitle{text-overflow:ellipsis;white-space:nowrap;text-transform:uppercase;letter-spacing:.04em;min-width:0;color:var(--fe-muted);flex:1;font-size:11px;font-weight:600;overflow:hidden}.Myjg4a_headerActions{flex:none;align-items:center;gap:2px;display:flex}.Myjg4a_action{appearance:none;color:var(--fe-muted);cursor:pointer;background:0 0;border:none;border-radius:5px;justify-content:center;align-items:center;width:22px;height:22px;padding:0;font-size:13px;line-height:1;display:inline-flex}.Myjg4a_action:hover:not(:disabled){background:var(--fe-hover);color:var(--fe-fg)}.Myjg4a_action:disabled{opacity:.5;cursor:default}.Myjg4a_treeArea{flex:1;min-height:0;padding:4px;overflow-y:auto}.Myjg4a_row{cursor:pointer;user-select:none;white-space:nowrap;height:22px;color:var(--fe-fg);border-radius:3px;align-items:center;gap:6px;font-size:13px;display:flex}.Myjg4a_row:hover{background:var(--fe-hover)}.Myjg4a_rowSelected{background:var(--fe-selection)}.Myjg4a_rowSelected .Myjg4a_name{color:var(--fe-fg)}.Myjg4a_chevron{text-align:center;width:12px;color:var(--fe-muted);flex:none;font-size:10px;line-height:22px}.Myjg4a_icon{text-align:center;flex:none;width:16px;font-size:13px;line-height:22px}.Myjg4a_fileBadge{letter-spacing:-.3px;box-sizing:border-box;vertical-align:middle;user-select:none;border-radius:3px;justify-content:center;align-items:center;width:16px;height:15px;padding:0 1px;font-size:8px;font-weight:700;line-height:1;display:inline-flex}.Myjg4a_fileBadgeLong{letter-spacing:-.4px;font-size:6.5px}.Myjg4a_name{text-overflow:ellipsis;flex:1;min-width:0;overflow:hidden}.Myjg4a_size{color:var(--fe-faint);flex:none;font-size:11px}.Myjg4a_actions{opacity:0;flex:none;align-items:center;gap:2px;transition:opacity .12s;display:flex}.Myjg4a_row:hover .Myjg4a_actions,.Myjg4a_row:focus-within .Myjg4a_actions{opacity:1}.Myjg4a_rowHint{color:var(--fe-faint);padding:2px 8px 2px 0;font-size:12px;line-height:20px}.Myjg4a_rowError{color:#f48771;padding:2px 8px 2px 0;font-size:12px;line-height:18px}.Myjg4a_preview{border-right:1px solid var(--fe-border);background:var(--fe-bg);flex-direction:column;flex:0 0 55%;min-width:240px;min-height:0;animation:.18s Myjg4a_fe-preview-in;display:flex;overflow:hidden}.Myjg4a_previewClosing{animation:.2s forwards Myjg4a_fe-preview-out}@keyframes Myjg4a_fe-preview-in{0%{opacity:0}to{opacity:1}}@keyframes Myjg4a_fe-preview-out{0%{opacity:1}to{opacity:0}}.Myjg4a_handle{cursor:col-resize;touch-action:none;z-index:2;background:0 0;flex:none;width:6px;margin-left:-4px;margin-right:-2px}.Myjg4a_handle:hover,.Myjg4a_handle:active{background:var(--fe-accent);opacity:.35}.Myjg4a_tabBar{border-bottom:1px solid var(--fe-border);background:var(--fe-sidebar-bg);flex:none;align-items:stretch;height:34px;display:flex}.Myjg4a_tabScroller{scrollbar-width:thin;flex:1;align-items:stretch;min-width:0;display:flex;overflow-x:auto}.Myjg4a_collapse,.Myjg4a_tabAction{appearance:none;color:var(--fe-muted);cursor:pointer;background:0 0;border:none;border-radius:4px;flex:none;justify-content:center;align-items:center;width:26px;height:26px;margin:auto 4px;padding:0;font-size:14px;line-height:1;display:inline-flex}.Myjg4a_collapse:hover,.Myjg4a_tabAction:hover{background:var(--fe-hover);color:var(--fe-fg)}.Myjg4a_tabActionActive{background:var(--fe-hover);color:var(--fe-accent)}.Myjg4a_tab{max-width:180px;color:var(--fe-muted);border-right:1px solid var(--fe-border);cursor:pointer;user-select:none;white-space:nowrap;flex:none;align-items:center;gap:6px;padding:0 8px 0 12px;font-size:13px;display:flex}.Myjg4a_tab:hover{background:var(--fe-hover)}.Myjg4a_tabActive{background:var(--fe-bg);color:var(--fe-fg);box-shadow:inset 0 -1px 0 var(--fe-accent)}.Myjg4a_tabName{text-overflow:ellipsis;flex:1;min-width:0;overflow:hidden}.Myjg4a_tabIcon{flex:none;justify-content:center;align-items:center;width:16px;font-size:13px;line-height:1;display:inline-flex}.Myjg4a_tabClose{appearance:none;color:var(--fe-faint);cursor:pointer;background:0 0;border:none;border-radius:3px;justify-content:center;align-items:center;width:16px;height:16px;padding:0;font-size:11px;display:inline-flex}.Myjg4a_tabClose:hover{background:var(--fe-hover);color:var(--fe-fg)}.Myjg4a_tabDot{background:var(--fe-fg);opacity:.85;border-radius:50%;flex:none;width:8px;height:8px}.Myjg4a_previewBody{flex:1;min-height:0;overflow:auto}.Myjg4a_editor{contain:layout paint;height:100%;min-height:0;position:relative}.Myjg4a_editorPlain .Myjg4a_editorTextarea{color:var(--fe-fg)}.Myjg4a_editorHint{background:var(--fe-hover);color:var(--fe-muted);pointer-events:none;border-radius:4px;padding:4px 10px;font-size:11px;position:absolute;bottom:8px;left:8px}.Myjg4a_editorHighlight{pointer-events:none;white-space:pre;tab-size:2;color:var(--fe-fg);background:0 0;margin:0;padding:12px 12px calc(12px + 1.5em);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;line-height:1.5;position:absolute;inset:0;overflow:auto}.Myjg4a_editorHighlight code{font-family:inherit;font-size:inherit;line-height:inherit;white-space:inherit}.Myjg4a_editorTextarea{box-sizing:border-box;resize:none;color:#0000;caret-color:var(--fe-fg);white-space:pre;tab-size:2;background:0 0;border:none;outline:none;margin:0;padding:12px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;line-height:1.5;position:absolute;inset:0;overflow:auto}.Myjg4a_editor[data-wrap=on] .Myjg4a_editorHighlight,.Myjg4a_editor[data-wrap=on] .Myjg4a_editorTextarea{white-space:pre-wrap;overflow-wrap:anywhere}.Myjg4a_editor[data-wrap=on] .Myjg4a_editorTextarea{overflow-x:hidden}.Myjg4a_saveError{color:#1e1e1e;background:#f48771;border-radius:4px;padding:4px 10px;font-size:12px;position:absolute;bottom:8px;right:8px}.Myjg4a_previewHint{color:var(--fe-faint);padding:12px;font-size:12px;line-height:18px}.Myjg4a_previewMeta{color:var(--fe-faint);font-size:11px}.Myjg4a_placeholder{text-align:center;color:var(--fe-faint);flex:1;justify-content:center;align-items:center;padding:16px;font-size:12px;line-height:18px;display:flex}.Myjg4a_editorHighlight .hljs-keyword,.Myjg4a_editorHighlight .hljs-selector-tag,.Myjg4a_editorHighlight .hljs-literal{color:var(--fe-token-keyword)}.Myjg4a_editorHighlight .hljs-string,.Myjg4a_editorHighlight .hljs-regexp,.Myjg4a_editorHighlight .hljs-addition{color:var(--fe-token-string)}.Myjg4a_editorHighlight .hljs-comment,.Myjg4a_editorHighlight .hljs-quote{color:var(--fe-token-comment);font-style:italic}.Myjg4a_editorHighlight .hljs-number,.Myjg4a_editorHighlight .hljs-symbol,.Myjg4a_editorHighlight .hljs-bullet{color:var(--fe-token-number)}.Myjg4a_editorHighlight .hljs-title,.Myjg4a_editorHighlight .hljs-section,.Myjg4a_editorHighlight .hljs-title.function_{color:var(--fe-token-function)}.Myjg4a_editorHighlight .hljs-type,.Myjg4a_editorHighlight .hljs-built_in,.Myjg4a_editorHighlight .hljs-class .hljs-title{color:var(--fe-token-type)}.Myjg4a_editorHighlight .hljs-attr,.Myjg4a_editorHighlight .hljs-attribute,.Myjg4a_editorHighlight .hljs-variable,.Myjg4a_editorHighlight .hljs-template-variable,.Myjg4a_editorHighlight .hljs-params{color:var(--fe-token-variable)}.Myjg4a_editorHighlight .hljs-tag,.Myjg4a_editorHighlight .hljs-name{color:var(--fe-token-tag)}.Myjg4a_editorHighlight .hljs-meta{color:var(--fe-token-meta)}.Myjg4a_editorHighlight .hljs-emphasis{font-style:italic}.Myjg4a_editorHighlight .hljs-strong{color:var(--fe-accent);font-weight:inherit}body[data-dsh-maid-atelier] [data-skin-chrome=top-trim],body[data-dsh-maid-atelier] [data-skin-chrome=bottom-trim]{translate:calc(var(--maid-sidebar-width,0px) + var(--dsh-explorer-width,0px) + var(--dsh-preview-width,0px)) 0!important}[class*=explorerCol]{border-right:1px solid var(--fe-border)!important}";
34
34
  const tagId = "dsh-plugin-workbench/files.module.css";
35
35
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
36
36
  const tag = document.createElement("style");
@@ -47,9 +47,13 @@ window.__ModuleLoader__.load({
47
47
  "column": "Myjg4a_column",
48
48
  "editor": "Myjg4a_editor",
49
49
  "editorHighlight": "Myjg4a_editorHighlight",
50
+ "editorHint": "Myjg4a_editorHint",
51
+ "editorPlain": "Myjg4a_editorPlain",
50
52
  "editorTextarea": "Myjg4a_editorTextarea",
51
53
  "fe-preview-in": "Myjg4a_fe-preview-in",
52
54
  "fe-preview-out": "Myjg4a_fe-preview-out",
55
+ "fileBadge": "Myjg4a_fileBadge",
56
+ "fileBadgeLong": "Myjg4a_fileBadgeLong",
53
57
  "handle": "Myjg4a_handle",
54
58
  "header": "Myjg4a_header",
55
59
  "headerActions": "Myjg4a_headerActions",
@@ -69,15 +73,465 @@ window.__ModuleLoader__.load({
69
73
  "saveError": "Myjg4a_saveError",
70
74
  "size": "Myjg4a_size",
71
75
  "tab": "Myjg4a_tab",
76
+ "tabAction": "Myjg4a_tabAction",
77
+ "tabActionActive": "Myjg4a_tabActionActive",
72
78
  "tabActive": "Myjg4a_tabActive",
73
79
  "tabBar": "Myjg4a_tabBar",
74
80
  "tabClose": "Myjg4a_tabClose",
75
81
  "tabDot": "Myjg4a_tabDot",
82
+ "tabIcon": "Myjg4a_tabIcon",
76
83
  "tabName": "Myjg4a_tabName",
77
84
  "tabScroller": "Myjg4a_tabScroller",
78
85
  "treeArea": "Myjg4a_treeArea"
79
86
  };
80
87
  //#endregion
88
+ //#region src/client/fileIcons.tsx
89
+ /**
90
+ * File-type icons for the explorer tree rows and the preview tabs.
91
+ *
92
+ * Code/config files render as small colored badges with an extension label
93
+ * (VS Code "Minimal"-theme style); media, archives and installer files use
94
+ * emoji; anything unknown falls back to a generic document glyph. No icon
95
+ * assets are shipped — badges are pure CSS (per-language colors) and emoji
96
+ * come from the platform font.
97
+ */
98
+ /** lowercase extension (no dot) → badge colors. */
99
+ const BADGES = {
100
+ ts: {
101
+ label: "TS",
102
+ bg: "#3178c6",
103
+ fg: "#ffffff"
104
+ },
105
+ mts: {
106
+ label: "TS",
107
+ bg: "#3178c6",
108
+ fg: "#ffffff"
109
+ },
110
+ cts: {
111
+ label: "TS",
112
+ bg: "#3178c6",
113
+ fg: "#ffffff"
114
+ },
115
+ tsx: {
116
+ label: "TSX",
117
+ bg: "#61dafb",
118
+ fg: "#06283d"
119
+ },
120
+ js: {
121
+ label: "JS",
122
+ bg: "#f7df1e",
123
+ fg: "#1e1e1e"
124
+ },
125
+ mjs: {
126
+ label: "JS",
127
+ bg: "#f7df1e",
128
+ fg: "#1e1e1e"
129
+ },
130
+ cjs: {
131
+ label: "JS",
132
+ bg: "#f7df1e",
133
+ fg: "#1e1e1e"
134
+ },
135
+ jsx: {
136
+ label: "JSX",
137
+ bg: "#61dafb",
138
+ fg: "#06283d"
139
+ },
140
+ json: {
141
+ label: "{}",
142
+ bg: "#cbcb41",
143
+ fg: "#1e1e1e"
144
+ },
145
+ jsonc: {
146
+ label: "{}",
147
+ bg: "#cbcb41",
148
+ fg: "#1e1e1e"
149
+ },
150
+ html: {
151
+ label: "HTML",
152
+ bg: "#e44d26",
153
+ fg: "#ffffff"
154
+ },
155
+ htm: {
156
+ label: "HTML",
157
+ bg: "#e44d26",
158
+ fg: "#ffffff"
159
+ },
160
+ css: {
161
+ label: "CSS",
162
+ bg: "#2965f1",
163
+ fg: "#ffffff"
164
+ },
165
+ scss: {
166
+ label: "SCSS",
167
+ bg: "#cc6699",
168
+ fg: "#ffffff"
169
+ },
170
+ less: {
171
+ label: "LESS",
172
+ bg: "#2a4d80",
173
+ fg: "#ffffff"
174
+ },
175
+ md: {
176
+ label: "MD",
177
+ bg: "#519aba",
178
+ fg: "#ffffff"
179
+ },
180
+ markdown: {
181
+ label: "MD",
182
+ bg: "#519aba",
183
+ fg: "#ffffff"
184
+ },
185
+ py: {
186
+ label: "PY",
187
+ bg: "#3572a5",
188
+ fg: "#ffffff"
189
+ },
190
+ pyw: {
191
+ label: "PY",
192
+ bg: "#3572a5",
193
+ fg: "#ffffff"
194
+ },
195
+ go: {
196
+ label: "GO",
197
+ bg: "#00add8",
198
+ fg: "#06283d"
199
+ },
200
+ rs: {
201
+ label: "RS",
202
+ bg: "#dea584",
203
+ fg: "#1e1e1e"
204
+ },
205
+ java: {
206
+ label: "JAVA",
207
+ bg: "#b07219",
208
+ fg: "#ffffff"
209
+ },
210
+ c: {
211
+ label: "C",
212
+ bg: "#6e6e6e",
213
+ fg: "#ffffff"
214
+ },
215
+ h: {
216
+ label: "H",
217
+ bg: "#6e6e6e",
218
+ fg: "#ffffff"
219
+ },
220
+ cpp: {
221
+ label: "CPP",
222
+ bg: "#f34b7d",
223
+ fg: "#ffffff"
224
+ },
225
+ cc: {
226
+ label: "CPP",
227
+ bg: "#f34b7d",
228
+ fg: "#ffffff"
229
+ },
230
+ cxx: {
231
+ label: "CPP",
232
+ bg: "#f34b7d",
233
+ fg: "#ffffff"
234
+ },
235
+ hpp: {
236
+ label: "HPP",
237
+ bg: "#f34b7d",
238
+ fg: "#ffffff"
239
+ },
240
+ hxx: {
241
+ label: "HPP",
242
+ bg: "#f34b7d",
243
+ fg: "#ffffff"
244
+ },
245
+ cs: {
246
+ label: "CS",
247
+ bg: "#178600",
248
+ fg: "#ffffff"
249
+ },
250
+ sh: {
251
+ label: "SH",
252
+ bg: "#89e051",
253
+ fg: "#1e1e1e"
254
+ },
255
+ bash: {
256
+ label: "SH",
257
+ bg: "#89e051",
258
+ fg: "#1e1e1e"
259
+ },
260
+ zsh: {
261
+ label: "SH",
262
+ bg: "#89e051",
263
+ fg: "#1e1e1e"
264
+ },
265
+ ps1: {
266
+ label: "PS1",
267
+ bg: "#012456",
268
+ fg: "#ffffff"
269
+ },
270
+ psm1: {
271
+ label: "PS1",
272
+ bg: "#012456",
273
+ fg: "#ffffff"
274
+ },
275
+ psd1: {
276
+ label: "PS1",
277
+ bg: "#012456",
278
+ fg: "#ffffff"
279
+ },
280
+ bat: {
281
+ label: "BAT",
282
+ bg: "#6e6e6e",
283
+ fg: "#ffffff"
284
+ },
285
+ cmd: {
286
+ label: "BAT",
287
+ bg: "#6e6e6e",
288
+ fg: "#ffffff"
289
+ },
290
+ sql: {
291
+ label: "SQL",
292
+ bg: "#e38c00",
293
+ fg: "#ffffff"
294
+ },
295
+ yml: {
296
+ label: "YML",
297
+ bg: "#cb171e",
298
+ fg: "#ffffff"
299
+ },
300
+ yaml: {
301
+ label: "YML",
302
+ bg: "#cb171e",
303
+ fg: "#ffffff"
304
+ },
305
+ toml: {
306
+ label: "TOML",
307
+ bg: "#8b8b8b",
308
+ fg: "#ffffff"
309
+ },
310
+ ini: {
311
+ label: "INI",
312
+ bg: "#8b8b8b",
313
+ fg: "#ffffff"
314
+ },
315
+ conf: {
316
+ label: "INI",
317
+ bg: "#8b8b8b",
318
+ fg: "#ffffff"
319
+ },
320
+ cfg: {
321
+ label: "INI",
322
+ bg: "#8b8b8b",
323
+ fg: "#ffffff"
324
+ },
325
+ xml: {
326
+ label: "XML",
327
+ bg: "#a074c4",
328
+ fg: "#ffffff"
329
+ },
330
+ xsl: {
331
+ label: "XML",
332
+ bg: "#a074c4",
333
+ fg: "#ffffff"
334
+ },
335
+ svg: {
336
+ label: "SVG",
337
+ bg: "#e37933",
338
+ fg: "#ffffff"
339
+ },
340
+ vue: {
341
+ label: "VUE",
342
+ bg: "#41b883",
343
+ fg: "#06283d"
344
+ },
345
+ lock: {
346
+ label: "LOCK",
347
+ bg: "#9d9d9d",
348
+ fg: "#1e1e1e"
349
+ },
350
+ env: {
351
+ label: "ENV",
352
+ bg: "#f05033",
353
+ fg: "#ffffff"
354
+ },
355
+ gitignore: {
356
+ label: "GIT",
357
+ bg: "#f05033",
358
+ fg: "#ffffff"
359
+ },
360
+ gitattributes: {
361
+ label: "GIT",
362
+ bg: "#f05033",
363
+ fg: "#ffffff"
364
+ },
365
+ gitmodules: {
366
+ label: "GIT",
367
+ bg: "#f05033",
368
+ fg: "#ffffff"
369
+ },
370
+ editorconfig: {
371
+ label: "CFG",
372
+ bg: "#8b8b8b",
373
+ fg: "#ffffff"
374
+ },
375
+ npmrc: {
376
+ label: "CFG",
377
+ bg: "#8b8b8b",
378
+ fg: "#ffffff"
379
+ },
380
+ pnpmrc: {
381
+ label: "CFG",
382
+ bg: "#8b8b8b",
383
+ fg: "#ffffff"
384
+ },
385
+ yarnrc: {
386
+ label: "CFG",
387
+ bg: "#8b8b8b",
388
+ fg: "#ffffff"
389
+ },
390
+ mk: {
391
+ label: "MK",
392
+ bg: "#6d8086",
393
+ fg: "#ffffff"
394
+ },
395
+ makefile: {
396
+ label: "MK",
397
+ bg: "#6d8086",
398
+ fg: "#ffffff"
399
+ },
400
+ txt: {
401
+ label: "TXT",
402
+ bg: "#9d9d9d",
403
+ fg: "#ffffff"
404
+ },
405
+ log: {
406
+ label: "TXT",
407
+ bg: "#9d9d9d",
408
+ fg: "#ffffff"
409
+ },
410
+ pdf: {
411
+ label: "PDF",
412
+ bg: "#e74c3c",
413
+ fg: "#ffffff"
414
+ }
415
+ };
416
+ /** lowercase extension (no dot) → emoji glyph. */
417
+ const EMOJI = {
418
+ png: "🖼️",
419
+ jpg: "🖼️",
420
+ jpeg: "🖼️",
421
+ gif: "🖼️",
422
+ webp: "🖼️",
423
+ bmp: "🖼️",
424
+ ico: "🖼️",
425
+ avif: "🖼️",
426
+ jfif: "🖼️",
427
+ tif: "🖼️",
428
+ tiff: "🖼️",
429
+ mp3: "🎵",
430
+ wav: "🎵",
431
+ flac: "🎵",
432
+ ogg: "🎵",
433
+ oga: "🎵",
434
+ m4a: "🎵",
435
+ aac: "🎵",
436
+ wma: "🎵",
437
+ opus: "🎵",
438
+ mid: "🎵",
439
+ mp4: "🎬",
440
+ mkv: "🎬",
441
+ avi: "🎬",
442
+ mov: "🎬",
443
+ webm: "🎬",
444
+ flv: "🎬",
445
+ wmv: "🎬",
446
+ m4v: "🎬",
447
+ mpg: "🎬",
448
+ mpeg: "🎬",
449
+ zip: "🗜️",
450
+ rar: "🗜️",
451
+ "7z": "🗜️",
452
+ tar: "🗜️",
453
+ gz: "🗜️",
454
+ bz2: "🗜️",
455
+ xz: "🗜️",
456
+ tgz: "🗜️",
457
+ tbz2: "🗜️",
458
+ zst: "🗜️",
459
+ doc: "📘",
460
+ docx: "📘",
461
+ odt: "📘",
462
+ rtf: "📘",
463
+ xls: "📗",
464
+ xlsx: "📗",
465
+ ods: "📗",
466
+ csv: "📗",
467
+ ppt: "📙",
468
+ pptx: "📙",
469
+ odp: "📙",
470
+ epub: "📖",
471
+ mobi: "📖",
472
+ exe: "📦",
473
+ msi: "📦",
474
+ dmg: "📦",
475
+ app: "📦",
476
+ deb: "📦",
477
+ rpm: "📦",
478
+ apk: "📦",
479
+ iso: "📦"
480
+ };
481
+ /** Well-known extension-less names (lowercase) → emoji. */
482
+ const NAMED_EMOJI = { dockerfile: "🐳" };
483
+ /** Resolve a file name to its icon spec (badge colors or an emoji glyph). */
484
+ function fileIconFor(name) {
485
+ const lower = name.trim().toLowerCase();
486
+ if (lower === "") return {
487
+ kind: "emoji",
488
+ char: "📄"
489
+ };
490
+ const namedEmoji = NAMED_EMOJI[lower];
491
+ if (namedEmoji !== void 0) return {
492
+ kind: "emoji",
493
+ char: namedEmoji
494
+ };
495
+ let key;
496
+ if (lower.startsWith(".")) {
497
+ const rest = lower.slice(1);
498
+ key = rest.startsWith("env.") ? "env" : rest;
499
+ } else {
500
+ const dot = lower.lastIndexOf(".");
501
+ key = dot > 0 ? lower.slice(dot + 1) : lower;
502
+ }
503
+ const badge = BADGES[key];
504
+ if (badge !== void 0) return {
505
+ kind: "badge",
506
+ label: badge.label,
507
+ bg: badge.bg,
508
+ fg: badge.fg
509
+ };
510
+ const emoji = EMOJI[key];
511
+ if (emoji !== void 0) return {
512
+ kind: "emoji",
513
+ char: emoji
514
+ };
515
+ return {
516
+ kind: "emoji",
517
+ char: "📄"
518
+ };
519
+ }
520
+ /** Render a file's icon: a colored extension badge or an emoji glyph. */
521
+ function FileIcon({ name }) {
522
+ const icon = fileIconFor(name);
523
+ if (icon.kind === "emoji") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: icon.char });
524
+ const className = icon.label.length >= 4 ? `${files_module_css_default.fileBadge} ${files_module_css_default.fileBadgeLong}` : files_module_css_default.fileBadge;
525
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
526
+ className,
527
+ style: {
528
+ background: icon.bg,
529
+ color: icon.fg
530
+ },
531
+ children: icon.label
532
+ });
533
+ }
534
+ //#endregion
81
535
  //#region src/client/store.ts
82
536
  /**
83
537
  * Module-local store shared by the explorer tree (FileExplorer) and the split
@@ -93,15 +547,29 @@ window.__ModuleLoader__.load({
93
547
  active: void 0,
94
548
  collapsed: false
95
549
  };
550
+ const WRAP_KEY = "dsh-plugin-workbench:wrap";
551
+ /** Read the persisted wrap preference; defaults to soft wrap ON. */
552
+ function storedWrap() {
553
+ try {
554
+ if (typeof window === "undefined") return true;
555
+ const stored = window.localStorage.getItem(WRAP_KEY);
556
+ return stored === null ? true : stored === "1";
557
+ } catch {
558
+ return true;
559
+ }
560
+ }
561
+ const initialWrap = storedWrap();
96
562
  let state = {
97
563
  currentCwd: void 0,
98
564
  workspaces: {},
99
- theme: "dark"
565
+ theme: "dark",
566
+ wrap: initialWrap
100
567
  };
101
568
  let view = {
102
569
  tabs: EMPTY_TABS,
103
570
  active: void 0,
104
571
  collapsed: false,
572
+ wrap: initialWrap,
105
573
  theme: "dark",
106
574
  cwd: void 0
107
575
  };
@@ -128,10 +596,11 @@ window.__ModuleLoader__.load({
128
596
  tabs: ws.tabs,
129
597
  active: ws.active,
130
598
  collapsed: ws.collapsed,
599
+ wrap: state.wrap,
131
600
  theme: state.theme,
132
601
  cwd: state.currentCwd
133
602
  };
134
- if (nextView.tabs === view.tabs && nextView.active === view.active && nextView.collapsed === view.collapsed && nextView.theme === view.theme && nextView.cwd === view.cwd) return;
603
+ if (nextView.tabs === view.tabs && nextView.active === view.active && nextView.collapsed === view.collapsed && nextView.wrap === view.wrap && nextView.theme === view.theme && nextView.cwd === view.cwd) return;
135
604
  view = nextView;
136
605
  for (const listener of [...listeners]) listener();
137
606
  }
@@ -236,6 +705,17 @@ window.__ModuleLoader__.load({
236
705
  theme: state.theme === "dark" ? "light" : "dark"
237
706
  });
238
707
  }
708
+ /** Toggle display-only soft wrap for the editor (global, persisted). */
709
+ function toggleWrap() {
710
+ const wrap = !state.wrap;
711
+ try {
712
+ window.localStorage.setItem(WRAP_KEY, wrap ? "1" : "0");
713
+ } catch {}
714
+ commit({
715
+ ...state,
716
+ wrap
717
+ });
718
+ }
239
719
  //#endregion
240
720
  //#region src/client/FileExplorer.tsx
241
721
  /**
@@ -257,6 +737,10 @@ window.__ModuleLoader__.load({
257
737
  }
258
738
  /** How often the visible tree is re-listed to pick up disk changes. */
259
739
  const REFRESH_MS = 2e3;
740
+ /** How many previously-expanded folders to re-list at once on workspace switch. */
741
+ const DIR_LOAD_BATCH = 4;
742
+ /** Pause between batches when restoring expanded folders. */
743
+ const DIR_LOAD_GAP_MS = 50;
260
744
  const EMPTY_EXPANDED = /* @__PURE__ */ new Set();
261
745
  /** True when two directory listings are identical (name/kind/size). */
262
746
  function sameEntries(a, b) {
@@ -264,6 +748,69 @@ window.__ModuleLoader__.load({
264
748
  for (let i = 0; i < a.length; i += 1) if (a[i].name !== b[i].name || a[i].kind !== b[i].kind || a[i].size !== b[i].size) return false;
265
749
  return true;
266
750
  }
751
+ /**
752
+ * One tree row, memoized so opening/activating a file (which changes the
753
+ * active-path highlight) re-renders only the affected row — with a workspace
754
+ * full of files, re-rendering the whole tree on every click is what made
755
+ * opening files feel laggy.
756
+ */
757
+ const TreeRow = (0, react.memo)(function TreeRow({ entry, depth, isActive, isExpanded, onToggleDir, onRefreshDir, onSelect, onDoubleClick, onOpenExternal, t }) {
758
+ const isDir = entry.kind === "dir";
759
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
760
+ className: `${files_module_css_default.row} ${isActive ? files_module_css_default.rowSelected : ""}`,
761
+ style: { paddingLeft: 8 + depth * 14 },
762
+ onClick: () => onSelect(entry),
763
+ onDoubleClick: () => onDoubleClick(entry),
764
+ role: "treeitem",
765
+ "aria-selected": isActive,
766
+ "aria-expanded": isDir ? isExpanded : void 0,
767
+ title: entry.name,
768
+ children: [
769
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
770
+ className: files_module_css_default.chevron,
771
+ onClick: (e) => {
772
+ e.stopPropagation();
773
+ if (isDir) onToggleDir(entry.path);
774
+ },
775
+ children: isDir ? isExpanded ? "▾" : "▸" : ""
776
+ }),
777
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
778
+ className: files_module_css_default.icon,
779
+ children: isDir ? isExpanded ? "📂" : "📁" : entry.kind === "file" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FileIcon, { name: entry.name }) : "·"
780
+ }),
781
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
782
+ className: files_module_css_default.name,
783
+ children: entry.name
784
+ }),
785
+ entry.kind === "file" && entry.size !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
786
+ className: files_module_css_default.size,
787
+ children: formatSize$1(entry.size)
788
+ }),
789
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
790
+ className: files_module_css_default.actions,
791
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
792
+ type: "button",
793
+ className: files_module_css_default.action,
794
+ title: t("action.open"),
795
+ onClick: (e) => {
796
+ e.stopPropagation();
797
+ onOpenExternal(entry.path);
798
+ },
799
+ children: "↗"
800
+ }), isDir && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
801
+ type: "button",
802
+ className: files_module_css_default.action,
803
+ title: t("action.refresh"),
804
+ onClick: (e) => {
805
+ e.stopPropagation();
806
+ onRefreshDir(entry.path);
807
+ },
808
+ children: "⟳"
809
+ })]
810
+ })
811
+ ]
812
+ });
813
+ });
267
814
  function FileExplorer({ width, useSessions, t, listDir, openPath }) {
268
815
  const sessionList = useSessions((s) => s);
269
816
  const currentId = sessionList.current;
@@ -291,6 +838,8 @@ window.__ModuleLoader__.load({
291
838
  expanded,
292
839
  rootLoading
293
840
  };
841
+ const childrenRef = (0, react.useRef)(children);
842
+ childrenRef.current = children;
294
843
  (0, react.useEffect)(() => {
295
844
  document.body.style.setProperty("--dsh-explorer-width", `${width}px`);
296
845
  return () => {
@@ -312,12 +861,18 @@ window.__ModuleLoader__.load({
312
861
  setRootLoading(true);
313
862
  const controller = new AbortController();
314
863
  rootAbortRef.current = controller;
315
- listDir(cwd, controller.signal).then((result) => {
864
+ listDir(cwd, controller.signal).then(async (result) => {
316
865
  if (controller.signal.aborted) return;
317
866
  setRoot(result.root);
318
867
  setChildren({ [result.root]: result.entries });
319
868
  setRootError(void 0);
320
- for (const dirPath of expanded) if (dirPath !== result.root) loadDir(dirPath);
869
+ const dirs = [...expanded].filter((dirPath) => dirPath !== result.root);
870
+ for (let i = 0; i < dirs.length; i += DIR_LOAD_BATCH) {
871
+ if (controller.signal.aborted) return;
872
+ const batch = dirs.slice(i, i + DIR_LOAD_BATCH);
873
+ await Promise.all(batch.map((dirPath) => loadDir(dirPath)));
874
+ if (i + DIR_LOAD_BATCH < dirs.length) await new Promise((resolve) => setTimeout(resolve, DIR_LOAD_GAP_MS));
875
+ }
321
876
  }).catch((error) => {
322
877
  if (controller.signal.aborted) return;
323
878
  setRootError(error instanceof Error ? error.message : String(error));
@@ -427,12 +982,8 @@ window.__ModuleLoader__.load({
427
982
  [cwdKey]: next
428
983
  };
429
984
  });
430
- if (!children[dirPath]) loadDir(dirPath);
431
- }, [
432
- cwdKey,
433
- children,
434
- loadDir
435
- ]);
985
+ if (!childrenRef.current[dirPath]) loadDir(dirPath);
986
+ }, [cwdKey, loadDir]);
436
987
  const refreshDir = (0, react.useCallback)((dirPath) => {
437
988
  setChildren((prev) => {
438
989
  const next = { ...prev };
@@ -468,59 +1019,17 @@ window.__ModuleLoader__.load({
468
1019
  const isLoading = isDir && loadingDirs.has(entry.path);
469
1020
  const childEntries = isDir && isExpanded ? children[entry.path] : void 0;
470
1021
  const error = isDir ? dirErrors[entry.path] : void 0;
471
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
472
- className: `${files_module_css_default.row} ${entry.kind === "file" && activePath === entry.path ? files_module_css_default.rowSelected : ""}`,
473
- style: { paddingLeft: 8 + depth * 14 },
474
- onClick: () => onRowClick(entry),
475
- onDoubleClick: () => onRowDoubleClick(entry),
476
- role: "treeitem",
477
- "aria-selected": entry.kind === "file" && activePath === entry.path,
478
- "aria-expanded": isDir ? isExpanded : void 0,
479
- title: entry.name,
480
- children: [
481
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
482
- className: files_module_css_default.chevron,
483
- onClick: (e) => {
484
- e.stopPropagation();
485
- if (isDir) toggleDir(entry.path);
486
- },
487
- children: isDir ? isExpanded ? "▾" : "▸" : ""
488
- }),
489
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
490
- className: files_module_css_default.icon,
491
- children: isDir ? "📁" : entry.kind === "file" ? "📄" : "·"
492
- }),
493
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
494
- className: files_module_css_default.name,
495
- children: entry.name
496
- }),
497
- entry.kind === "file" && entry.size !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
498
- className: files_module_css_default.size,
499
- children: formatSize$1(entry.size)
500
- }),
501
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
502
- className: files_module_css_default.actions,
503
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
504
- type: "button",
505
- className: files_module_css_default.action,
506
- title: t("action.open"),
507
- onClick: (e) => {
508
- e.stopPropagation();
509
- openPath(entry.path);
510
- },
511
- children: "↗"
512
- }), isDir && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
513
- type: "button",
514
- className: files_module_css_default.action,
515
- title: t("action.refresh"),
516
- onClick: (e) => {
517
- e.stopPropagation();
518
- refreshDir(entry.path);
519
- },
520
- children: "⟳"
521
- })]
522
- })
523
- ]
1022
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TreeRow, {
1023
+ entry,
1024
+ depth,
1025
+ isActive: entry.kind === "file" && activePath === entry.path,
1026
+ isExpanded,
1027
+ onToggleDir: toggleDir,
1028
+ onRefreshDir: refreshDir,
1029
+ onSelect: onRowClick,
1030
+ onDoubleClick: onRowDoubleClick,
1031
+ onOpenExternal: openPath,
1032
+ t
524
1033
  }), isDir && isExpanded && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
525
1034
  isLoading && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
526
1035
  className: files_module_css_default.rowHint,
@@ -8227,6 +8736,20 @@ window.__ModuleLoader__.load({
8227
8736
  const PREVIEW_MIN = 240;
8228
8737
  const CHAT_MIN = 240;
8229
8738
  const PREVIEW_TOO_LARGE_LABEL = "512KB";
8739
+ /**
8740
+ * Above this size the overlay editor (syntax-highlight layer + transparent
8741
+ * textarea) falls back to a plain textarea: re-injecting and re-laying out
8742
+ * hundreds of KB of wrapped text on every keystroke is what makes the page
8743
+ * lag. The plain textarea keeps editing, wrapping and scrolling — it only
8744
+ * loses the colors, which files this big rarely need anyway.
8745
+ */
8746
+ const HIGHLIGHT_MAX_BYTES = 65536;
8747
+ /**
8748
+ * Languages always rendered as a plain textarea, never the overlay: the
8749
+ * highlight layer costs a full extra layout pass (and with CJK text a
8750
+ * fragile alignment surface) for prose formats where colors add little.
8751
+ */
8752
+ const PLAIN_LANGUAGES = /* @__PURE__ */ new Set(["markdown"]);
8230
8753
  function basenameOf(path) {
8231
8754
  const trimmed = path.replace(/[\\/]+$/, "");
8232
8755
  const idx = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
@@ -8261,7 +8784,7 @@ window.__ModuleLoader__.load({
8261
8784
  };
8262
8785
  }
8263
8786
  function FilePreview({ t, readFile, writeFile }) {
8264
- const { tabs, active, theme, collapsed } = useTabsState();
8787
+ const { tabs, active, theme, collapsed, wrap } = useTabsState();
8265
8788
  const [previewWidth, setPreviewWidth] = (0, react.useState)(null);
8266
8789
  const [closing, setClosing] = (0, react.useState)(false);
8267
8790
  const [dragPath, setDragPath] = (0, react.useState)(void 0);
@@ -8272,35 +8795,38 @@ window.__ModuleLoader__.load({
8272
8795
  const previewRef = (0, react.useRef)(null);
8273
8796
  const handleRef = (0, react.useRef)(null);
8274
8797
  const highlightRef = (0, react.useRef)(null);
8798
+ const textareaRef = (0, react.useRef)(null);
8275
8799
  const refresh = (0, react.useCallback)(() => bump((v) => v + 1), []);
8276
8800
  (0, react.useEffect)(() => {
8277
8801
  const cache = cacheRef.current;
8278
8802
  for (const [path] of cache) if (!tabs.includes(path)) cache.delete(path);
8279
- const controllers = [];
8280
- for (const path of tabs) {
8281
- if (cache.has(path)) continue;
8282
- cache.set(path, { status: "loading" });
8283
- const controller = new AbortController();
8284
- controllers.push(controller);
8285
- readFile(path, controller.signal).then((result) => {
8286
- if (!controller.signal.aborted) {
8287
- cache.set(path, previewDataOf(result));
8288
- refresh();
8289
- }
8290
- }).catch((error) => {
8291
- if (!controller.signal.aborted) {
8292
- cache.set(path, {
8293
- status: "error",
8294
- message: error instanceof Error ? error.message : String(error)
8295
- });
8296
- refresh();
8297
- }
8298
- });
8803
+ const target = active ?? tabs[0];
8804
+ if (target === void 0 || cache.has(target)) {
8805
+ refresh();
8806
+ return;
8299
8807
  }
8808
+ const controller = new AbortController();
8809
+ cache.set(target, { status: "loading" });
8810
+ readFile(target, controller.signal).then((result) => {
8811
+ if (!controller.signal.aborted) {
8812
+ cache.set(target, previewDataOf(result));
8813
+ if (result.size > HIGHLIGHT_MAX_BYTES) requestAnimationFrame(refresh);
8814
+ else refresh();
8815
+ }
8816
+ }).catch((error) => {
8817
+ if (!controller.signal.aborted) {
8818
+ cache.set(target, {
8819
+ status: "error",
8820
+ message: error instanceof Error ? error.message : String(error)
8821
+ });
8822
+ refresh();
8823
+ }
8824
+ });
8300
8825
  refresh();
8301
- return () => controllers.forEach((c) => c.abort());
8826
+ return () => controller.abort();
8302
8827
  }, [
8303
8828
  tabs,
8829
+ active,
8304
8830
  readFile,
8305
8831
  refresh
8306
8832
  ]);
@@ -8320,6 +8846,28 @@ window.__ModuleLoader__.load({
8320
8846
  }
8321
8847
  }, [tabs.length]);
8322
8848
  const isOpen = !collapsed && (tabs.length > 0 || closing || hasOpenedRef.current);
8849
+ const activeData = active !== void 0 ? cacheRef.current.get(active) : void 0;
8850
+ const language = active !== void 0 ? detectLanguage(active) : void 0;
8851
+ const tooBig = (activeData?.size ?? 0) > HIGHLIGHT_MAX_BYTES;
8852
+ const plain = language === void 0 || tooBig || language !== void 0 && PLAIN_LANGUAGES.has(language);
8853
+ (0, react.useEffect)(() => {
8854
+ const pre = highlightRef.current;
8855
+ const ta = textareaRef.current;
8856
+ if (pre === null || ta === null || plain) return void 0;
8857
+ let raf = 0;
8858
+ const tick = () => {
8859
+ pre.scrollTop = ta.scrollTop;
8860
+ pre.scrollLeft = ta.scrollLeft;
8861
+ raf = requestAnimationFrame(tick);
8862
+ };
8863
+ raf = requestAnimationFrame(tick);
8864
+ return () => cancelAnimationFrame(raf);
8865
+ }, [
8866
+ isOpen,
8867
+ active,
8868
+ activeData?.status,
8869
+ plain
8870
+ ]);
8323
8871
  (0, react.useEffect)(() => {
8324
8872
  if (!isOpen) {
8325
8873
  document.body.style.removeProperty("--dsh-preview-width");
@@ -8416,7 +8964,6 @@ window.__ModuleLoader__.load({
8416
8964
  const onTabDragEnd = (0, react.useCallback)(() => {
8417
8965
  setDragPath(void 0);
8418
8966
  }, []);
8419
- const activeData = active !== void 0 ? cacheRef.current.get(active) : void 0;
8420
8967
  if (!isOpen) return null;
8421
8968
  const renderBody = () => {
8422
8969
  if (activeData === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -8428,34 +8975,36 @@ window.__ModuleLoader__.load({
8428
8975
  className: files_module_css_default.previewHint,
8429
8976
  children: t("preview.loading")
8430
8977
  });
8431
- case "loaded": {
8432
- const highlighted = active !== void 0 ? highlightCode(activeData.draft ?? "", active) : "";
8433
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8434
- className: files_module_css_default.editor,
8435
- children: [
8436
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
8437
- ref: highlightRef,
8438
- className: files_module_css_default.editorHighlight,
8439
- "aria-hidden": "true",
8440
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { dangerouslySetInnerHTML: { __html: highlighted } })
8441
- }),
8442
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
8443
- className: files_module_css_default.editorTextarea,
8444
- value: activeData.draft ?? "",
8445
- onChange: (e) => onTextareaChange(e.target.value),
8446
- onScroll: onTextareaScroll,
8447
- onKeyDown,
8448
- spellCheck: false,
8449
- wrap: "off",
8450
- title: t("action.saveHint")
8451
- }),
8452
- saveError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8453
- className: files_module_css_default.saveError,
8454
- children: saveError
8455
- })
8456
- ]
8457
- });
8458
- }
8978
+ case "loaded": return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8979
+ className: `${files_module_css_default.editor}${plain ? ` ${files_module_css_default.editorPlain}` : ""}`,
8980
+ "data-wrap": wrap ? "on" : "off",
8981
+ children: [
8982
+ !plain && active !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
8983
+ ref: highlightRef,
8984
+ className: files_module_css_default.editorHighlight,
8985
+ "aria-hidden": "true",
8986
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { dangerouslySetInnerHTML: { __html: highlightCode(activeData.draft ?? "", active) } })
8987
+ }),
8988
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
8989
+ ref: textareaRef,
8990
+ className: files_module_css_default.editorTextarea,
8991
+ value: activeData.draft ?? "",
8992
+ onChange: (e) => onTextareaChange(e.target.value),
8993
+ onScroll: onTextareaScroll,
8994
+ onKeyDown,
8995
+ spellCheck: false,
8996
+ wrap: wrap ? "soft" : "off"
8997
+ }),
8998
+ language !== void 0 && tooBig && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8999
+ className: files_module_css_default.editorHint,
9000
+ children: t("preview.highlightOff")
9001
+ }),
9002
+ saveError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9003
+ className: files_module_css_default.saveError,
9004
+ children: saveError
9005
+ })
9006
+ ]
9007
+ });
8459
9008
  case "binary": return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8460
9009
  className: files_module_css_default.previewHint,
8461
9010
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: t("preview.binary") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -8488,44 +9037,58 @@ window.__ModuleLoader__.load({
8488
9037
  "data-fe-theme": theme,
8489
9038
  children: [tabs.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8490
9039
  className: files_module_css_default.tabBar,
8491
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8492
- className: files_module_css_default.tabScroller,
8493
- children: tabs.map((path) => {
8494
- const data = cacheRef.current.get(path);
8495
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8496
- className: `${files_module_css_default.tab} ${path === active ? files_module_css_default.tabActive : ""}`,
8497
- onClick: () => activateFile(path),
8498
- draggable: true,
8499
- onDragStart: (e) => onTabDragStart(e, path),
8500
- onDragOver: (e) => onTabDragOver(e, path),
8501
- onDragEnd: onTabDragEnd,
8502
- title: path,
8503
- children: [
8504
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8505
- className: files_module_css_default.tabName,
8506
- children: basenameOf(path)
8507
- }),
8508
- data?.dirty === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: files_module_css_default.tabDot }),
8509
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8510
- type: "button",
8511
- className: files_module_css_default.tabClose,
8512
- title: t("tab.close"),
8513
- onClick: (e) => {
8514
- e.stopPropagation();
8515
- closeFile(path);
8516
- },
8517
- children: ""
8518
- })
8519
- ]
8520
- }, path);
9040
+ children: [
9041
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9042
+ className: files_module_css_default.tabScroller,
9043
+ children: tabs.map((path) => {
9044
+ const data = cacheRef.current.get(path);
9045
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9046
+ className: `${files_module_css_default.tab} ${path === active ? files_module_css_default.tabActive : ""}`,
9047
+ onClick: () => activateFile(path),
9048
+ draggable: true,
9049
+ onDragStart: (e) => onTabDragStart(e, path),
9050
+ onDragOver: (e) => onTabDragOver(e, path),
9051
+ onDragEnd: onTabDragEnd,
9052
+ title: path,
9053
+ children: [
9054
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
9055
+ className: files_module_css_default.tabIcon,
9056
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FileIcon, { name: basenameOf(path) })
9057
+ }),
9058
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
9059
+ className: files_module_css_default.tabName,
9060
+ children: basenameOf(path)
9061
+ }),
9062
+ data?.dirty === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: files_module_css_default.tabDot }),
9063
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
9064
+ type: "button",
9065
+ className: files_module_css_default.tabClose,
9066
+ title: t("tab.close"),
9067
+ onClick: (e) => {
9068
+ e.stopPropagation();
9069
+ closeFile(path);
9070
+ },
9071
+ children: "✕"
9072
+ })
9073
+ ]
9074
+ }, path);
9075
+ })
9076
+ }),
9077
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
9078
+ type: "button",
9079
+ className: `${files_module_css_default.tabAction}${wrap ? ` ${files_module_css_default.tabActionActive}` : ""}`,
9080
+ title: wrap ? t("action.wrapOff") : t("action.wrapOn"),
9081
+ onClick: toggleWrap,
9082
+ children: "⤶"
9083
+ }),
9084
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
9085
+ type: "button",
9086
+ className: files_module_css_default.collapse,
9087
+ title: t("tab.collapse"),
9088
+ onClick: collapsePreview,
9089
+ children: "<"
8521
9090
  })
8522
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8523
- type: "button",
8524
- className: files_module_css_default.collapse,
8525
- title: t("tab.collapse"),
8526
- onClick: collapsePreview,
8527
- children: "<"
8528
- })]
9091
+ ]
8529
9092
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8530
9093
  className: files_module_css_default.previewBody,
8531
9094
  children: renderBody()
@@ -8549,6 +9112,8 @@ window.__ModuleLoader__.load({
8549
9112
  "action.open": "在系统中打开",
8550
9113
  "action.theme": "切换浅色/暗色主题",
8551
9114
  "action.saveHint": "Ctrl+S 保存",
9115
+ "action.wrapOn": "开启自动换行(显示层换行,不改文件)",
9116
+ "action.wrapOff": "关闭自动换行(长行横向滚动)",
8552
9117
  "tab.close": "关闭标签页",
8553
9118
  "tab.collapse": "收起文件详情",
8554
9119
  "tab.expand": "弹出文件详情",
@@ -8556,7 +9121,8 @@ window.__ModuleLoader__.load({
8556
9121
  "preview.binary": "二进制文件,无法预览",
8557
9122
  "preview.tooLarge": "文件过大,仅支持预览不超过 {limit}",
8558
9123
  "preview.emptyDir": "空目录",
8559
- "preview.error": "加载失败"
9124
+ "preview.error": "加载失败",
9125
+ "preview.highlightOff": "大文件:已关闭语法高亮(仍可编辑)"
8560
9126
  };
8561
9127
  const en = {
8562
9128
  "placeholder.noSession": "No session",
@@ -8565,6 +9131,8 @@ window.__ModuleLoader__.load({
8565
9131
  "action.open": "Open in system",
8566
9132
  "action.theme": "Toggle light/dark theme",
8567
9133
  "action.saveHint": "Ctrl+S to save",
9134
+ "action.wrapOn": "Enable word wrap (display only, file unchanged)",
9135
+ "action.wrapOff": "Disable word wrap (long lines scroll horizontally)",
8568
9136
  "tab.close": "Close tab",
8569
9137
  "tab.collapse": "Collapse file details",
8570
9138
  "tab.expand": "Expand file details",
@@ -8572,7 +9140,8 @@ window.__ModuleLoader__.load({
8572
9140
  "preview.binary": "Binary file, cannot preview",
8573
9141
  "preview.tooLarge": "File too large to preview (limit {limit})",
8574
9142
  "preview.emptyDir": "Empty directory",
8575
- "preview.error": "Load failed"
9143
+ "preview.error": "Load failed",
9144
+ "preview.highlightOff": "Large file: syntax highlighting off (still editable)"
8576
9145
  };
8577
9146
  //#endregion
8578
9147
  //#region src/client/index.ts