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/CHANGELOG.md +32 -0
- package/README.md +5 -2
- package/lib/client.js +723 -154
- package/lib/client.js.map +1 -1
- package/package.json +3 -2
- package/scripts/verify-alignment.mjs +180 -0
- package/src/client/FileExplorer.tsx +127 -59
- package/src/client/FilePreview.tsx +97 -36
- package/src/client/fileIcons.tsx +144 -0
- package/src/client/files.module.css +96 -4
- package/src/client/locales.ts +6 -0
- package/src/client/store.ts +32 -2
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-type icons for the explorer tree rows and the preview tabs.
|
|
3
|
+
*
|
|
4
|
+
* Code/config files render as small colored badges with an extension label
|
|
5
|
+
* (VS Code "Minimal"-theme style); media, archives and installer files use
|
|
6
|
+
* emoji; anything unknown falls back to a generic document glyph. No icon
|
|
7
|
+
* assets are shipped — badges are pure CSS (per-language colors) and emoji
|
|
8
|
+
* come from the platform font.
|
|
9
|
+
*/
|
|
10
|
+
import styles from './files.module.css'
|
|
11
|
+
|
|
12
|
+
interface BadgeSpec {
|
|
13
|
+
label: string
|
|
14
|
+
bg: string
|
|
15
|
+
fg: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** lowercase extension (no dot) → badge colors. */
|
|
19
|
+
const BADGES: Record<string, BadgeSpec> = {
|
|
20
|
+
// code
|
|
21
|
+
ts: { label: 'TS', bg: '#3178c6', fg: '#ffffff' },
|
|
22
|
+
mts: { label: 'TS', bg: '#3178c6', fg: '#ffffff' },
|
|
23
|
+
cts: { label: 'TS', bg: '#3178c6', fg: '#ffffff' },
|
|
24
|
+
tsx: { label: 'TSX', bg: '#61dafb', fg: '#06283d' },
|
|
25
|
+
js: { label: 'JS', bg: '#f7df1e', fg: '#1e1e1e' },
|
|
26
|
+
mjs: { label: 'JS', bg: '#f7df1e', fg: '#1e1e1e' },
|
|
27
|
+
cjs: { label: 'JS', bg: '#f7df1e', fg: '#1e1e1e' },
|
|
28
|
+
jsx: { label: 'JSX', bg: '#61dafb', fg: '#06283d' },
|
|
29
|
+
json: { label: '{}', bg: '#cbcb41', fg: '#1e1e1e' },
|
|
30
|
+
jsonc: { label: '{}', bg: '#cbcb41', fg: '#1e1e1e' },
|
|
31
|
+
html: { label: 'HTML', bg: '#e44d26', fg: '#ffffff' },
|
|
32
|
+
htm: { label: 'HTML', bg: '#e44d26', fg: '#ffffff' },
|
|
33
|
+
css: { label: 'CSS', bg: '#2965f1', fg: '#ffffff' },
|
|
34
|
+
scss: { label: 'SCSS', bg: '#cc6699', fg: '#ffffff' },
|
|
35
|
+
less: { label: 'LESS', bg: '#2a4d80', fg: '#ffffff' },
|
|
36
|
+
md: { label: 'MD', bg: '#519aba', fg: '#ffffff' },
|
|
37
|
+
markdown: { label: 'MD', bg: '#519aba', fg: '#ffffff' },
|
|
38
|
+
py: { label: 'PY', bg: '#3572a5', fg: '#ffffff' },
|
|
39
|
+
pyw: { label: 'PY', bg: '#3572a5', fg: '#ffffff' },
|
|
40
|
+
go: { label: 'GO', bg: '#00add8', fg: '#06283d' },
|
|
41
|
+
rs: { label: 'RS', bg: '#dea584', fg: '#1e1e1e' },
|
|
42
|
+
java: { label: 'JAVA', bg: '#b07219', fg: '#ffffff' },
|
|
43
|
+
c: { label: 'C', bg: '#6e6e6e', fg: '#ffffff' },
|
|
44
|
+
h: { label: 'H', bg: '#6e6e6e', fg: '#ffffff' },
|
|
45
|
+
cpp: { label: 'CPP', bg: '#f34b7d', fg: '#ffffff' },
|
|
46
|
+
cc: { label: 'CPP', bg: '#f34b7d', fg: '#ffffff' },
|
|
47
|
+
cxx: { label: 'CPP', bg: '#f34b7d', fg: '#ffffff' },
|
|
48
|
+
hpp: { label: 'HPP', bg: '#f34b7d', fg: '#ffffff' },
|
|
49
|
+
hxx: { label: 'HPP', bg: '#f34b7d', fg: '#ffffff' },
|
|
50
|
+
cs: { label: 'CS', bg: '#178600', fg: '#ffffff' },
|
|
51
|
+
sh: { label: 'SH', bg: '#89e051', fg: '#1e1e1e' },
|
|
52
|
+
bash: { label: 'SH', bg: '#89e051', fg: '#1e1e1e' },
|
|
53
|
+
zsh: { label: 'SH', bg: '#89e051', fg: '#1e1e1e' },
|
|
54
|
+
ps1: { label: 'PS1', bg: '#012456', fg: '#ffffff' },
|
|
55
|
+
psm1: { label: 'PS1', bg: '#012456', fg: '#ffffff' },
|
|
56
|
+
psd1: { label: 'PS1', bg: '#012456', fg: '#ffffff' },
|
|
57
|
+
bat: { label: 'BAT', bg: '#6e6e6e', fg: '#ffffff' },
|
|
58
|
+
cmd: { label: 'BAT', bg: '#6e6e6e', fg: '#ffffff' },
|
|
59
|
+
sql: { label: 'SQL', bg: '#e38c00', fg: '#ffffff' },
|
|
60
|
+
yml: { label: 'YML', bg: '#cb171e', fg: '#ffffff' },
|
|
61
|
+
yaml: { label: 'YML', bg: '#cb171e', fg: '#ffffff' },
|
|
62
|
+
toml: { label: 'TOML', bg: '#8b8b8b', fg: '#ffffff' },
|
|
63
|
+
ini: { label: 'INI', bg: '#8b8b8b', fg: '#ffffff' },
|
|
64
|
+
conf: { label: 'INI', bg: '#8b8b8b', fg: '#ffffff' },
|
|
65
|
+
cfg: { label: 'INI', bg: '#8b8b8b', fg: '#ffffff' },
|
|
66
|
+
xml: { label: 'XML', bg: '#a074c4', fg: '#ffffff' },
|
|
67
|
+
xsl: { label: 'XML', bg: '#a074c4', fg: '#ffffff' },
|
|
68
|
+
svg: { label: 'SVG', bg: '#e37933', fg: '#ffffff' },
|
|
69
|
+
vue: { label: 'VUE', bg: '#41b883', fg: '#06283d' },
|
|
70
|
+
lock: { label: 'LOCK', bg: '#9d9d9d', fg: '#1e1e1e' },
|
|
71
|
+
env: { label: 'ENV', bg: '#f05033', fg: '#ffffff' },
|
|
72
|
+
gitignore: { label: 'GIT', bg: '#f05033', fg: '#ffffff' },
|
|
73
|
+
gitattributes: { label: 'GIT', bg: '#f05033', fg: '#ffffff' },
|
|
74
|
+
gitmodules: { label: 'GIT', bg: '#f05033', fg: '#ffffff' },
|
|
75
|
+
editorconfig: { label: 'CFG', bg: '#8b8b8b', fg: '#ffffff' },
|
|
76
|
+
npmrc: { label: 'CFG', bg: '#8b8b8b', fg: '#ffffff' },
|
|
77
|
+
pnpmrc: { label: 'CFG', bg: '#8b8b8b', fg: '#ffffff' },
|
|
78
|
+
yarnrc: { label: 'CFG', bg: '#8b8b8b', fg: '#ffffff' },
|
|
79
|
+
mk: { label: 'MK', bg: '#6d8086', fg: '#ffffff' },
|
|
80
|
+
makefile: { label: 'MK', bg: '#6d8086', fg: '#ffffff' },
|
|
81
|
+
txt: { label: 'TXT', bg: '#9d9d9d', fg: '#ffffff' },
|
|
82
|
+
log: { label: 'TXT', bg: '#9d9d9d', fg: '#ffffff' },
|
|
83
|
+
pdf: { label: 'PDF', bg: '#e74c3c', fg: '#ffffff' },
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** lowercase extension (no dot) → emoji glyph. */
|
|
87
|
+
const EMOJI: Record<string, string> = {
|
|
88
|
+
png: '🖼️', jpg: '🖼️', jpeg: '🖼️', gif: '🖼️', webp: '🖼️', bmp: '🖼️', ico: '🖼️', avif: '🖼️', jfif: '🖼️', tif: '🖼️', tiff: '🖼️',
|
|
89
|
+
mp3: '🎵', wav: '🎵', flac: '🎵', ogg: '🎵', oga: '🎵', m4a: '🎵', aac: '🎵', wma: '🎵', opus: '🎵', mid: '🎵',
|
|
90
|
+
mp4: '🎬', mkv: '🎬', avi: '🎬', mov: '🎬', webm: '🎬', flv: '🎬', wmv: '🎬', m4v: '🎬', mpg: '🎬', mpeg: '🎬',
|
|
91
|
+
zip: '🗜️', rar: '🗜️', '7z': '🗜️', tar: '🗜️', gz: '🗜️', bz2: '🗜️', xz: '🗜️', tgz: '🗜️', tbz2: '🗜️', zst: '🗜️',
|
|
92
|
+
doc: '📘', docx: '📘', odt: '📘', rtf: '📘',
|
|
93
|
+
xls: '📗', xlsx: '📗', ods: '📗', csv: '📗',
|
|
94
|
+
ppt: '📙', pptx: '📙', odp: '📙',
|
|
95
|
+
epub: '📖', mobi: '📖',
|
|
96
|
+
exe: '📦', msi: '📦', dmg: '📦', app: '📦', deb: '📦', rpm: '📦', apk: '📦', iso: '📦',
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Well-known extension-less names (lowercase) → emoji. */
|
|
100
|
+
const NAMED_EMOJI: Record<string, string> = {
|
|
101
|
+
dockerfile: '🐳',
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type FileIconSpec =
|
|
105
|
+
| { kind: 'badge'; label: string; bg: string; fg: string }
|
|
106
|
+
| { kind: 'emoji'; char: string }
|
|
107
|
+
|
|
108
|
+
/** Resolve a file name to its icon spec (badge colors or an emoji glyph). */
|
|
109
|
+
export function fileIconFor(name: string): FileIconSpec {
|
|
110
|
+
const lower = name.trim().toLowerCase()
|
|
111
|
+
if (lower === '') return { kind: 'emoji', char: '📄' }
|
|
112
|
+
|
|
113
|
+
const namedEmoji = NAMED_EMOJI[lower]
|
|
114
|
+
if (namedEmoji !== undefined) return { kind: 'emoji', char: namedEmoji }
|
|
115
|
+
|
|
116
|
+
// Dotfiles use the dotted remainder as the key (`.gitignore` → gitignore,
|
|
117
|
+
// `.env.local` → env), regular files use the last extension segment.
|
|
118
|
+
let key: string
|
|
119
|
+
if (lower.startsWith('.')) {
|
|
120
|
+
const rest = lower.slice(1)
|
|
121
|
+
key = rest.startsWith('env.') ? 'env' : rest
|
|
122
|
+
} else {
|
|
123
|
+
const dot = lower.lastIndexOf('.')
|
|
124
|
+
key = dot > 0 ? lower.slice(dot + 1) : lower
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const badge = BADGES[key]
|
|
128
|
+
if (badge !== undefined) return { kind: 'badge', label: badge.label, bg: badge.bg, fg: badge.fg }
|
|
129
|
+
const emoji = EMOJI[key]
|
|
130
|
+
if (emoji !== undefined) return { kind: 'emoji', char: emoji }
|
|
131
|
+
return { kind: 'emoji', char: '📄' }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Render a file's icon: a colored extension badge or an emoji glyph. */
|
|
135
|
+
export function FileIcon({ name }: { name: string }) {
|
|
136
|
+
const icon = fileIconFor(name)
|
|
137
|
+
if (icon.kind === 'emoji') return <>{icon.char}</>
|
|
138
|
+
const className = icon.label.length >= 4 ? `${styles.fileBadge} ${styles.fileBadgeLong}` : styles.fileBadge
|
|
139
|
+
return (
|
|
140
|
+
<span className={className} style={{ background: icon.bg, color: icon.fg }}>
|
|
141
|
+
{icon.label}
|
|
142
|
+
</span>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
@@ -170,6 +170,30 @@
|
|
|
170
170
|
line-height: 22px;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
/* Colored extension badge (VS Code "Minimal"-style file icon). */
|
|
174
|
+
.fileBadge {
|
|
175
|
+
display: inline-flex;
|
|
176
|
+
align-items: center;
|
|
177
|
+
justify-content: center;
|
|
178
|
+
width: 16px;
|
|
179
|
+
height: 15px;
|
|
180
|
+
border-radius: 3px;
|
|
181
|
+
font-size: 8px;
|
|
182
|
+
font-weight: 700;
|
|
183
|
+
line-height: 1;
|
|
184
|
+
letter-spacing: -0.3px;
|
|
185
|
+
padding: 0 1px;
|
|
186
|
+
box-sizing: border-box;
|
|
187
|
+
vertical-align: middle;
|
|
188
|
+
user-select: none;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* Four-character labels (HTML/JAVA/SCSS/LESS/TOML/…) need a smaller size. */
|
|
192
|
+
.fileBadgeLong {
|
|
193
|
+
font-size: 6.5px;
|
|
194
|
+
letter-spacing: -0.4px;
|
|
195
|
+
}
|
|
196
|
+
|
|
173
197
|
.name {
|
|
174
198
|
flex: 1;
|
|
175
199
|
min-width: 0;
|
|
@@ -281,7 +305,8 @@
|
|
|
281
305
|
scrollbar-width: thin;
|
|
282
306
|
}
|
|
283
307
|
|
|
284
|
-
.collapse
|
|
308
|
+
.collapse,
|
|
309
|
+
.tabAction {
|
|
285
310
|
flex: none;
|
|
286
311
|
appearance: none;
|
|
287
312
|
border: none;
|
|
@@ -300,11 +325,18 @@
|
|
|
300
325
|
padding: 0;
|
|
301
326
|
}
|
|
302
327
|
|
|
303
|
-
.collapse:hover
|
|
328
|
+
.collapse:hover,
|
|
329
|
+
.tabAction:hover {
|
|
304
330
|
background: var(--fe-hover);
|
|
305
331
|
color: var(--fe-fg);
|
|
306
332
|
}
|
|
307
333
|
|
|
334
|
+
/* Wrap toggle active state: soft wrap is on. */
|
|
335
|
+
.tabActionActive {
|
|
336
|
+
background: var(--fe-hover);
|
|
337
|
+
color: var(--fe-accent);
|
|
338
|
+
}
|
|
339
|
+
|
|
308
340
|
.tab {
|
|
309
341
|
flex: none;
|
|
310
342
|
display: flex;
|
|
@@ -337,6 +369,16 @@
|
|
|
337
369
|
text-overflow: ellipsis;
|
|
338
370
|
}
|
|
339
371
|
|
|
372
|
+
.tabIcon {
|
|
373
|
+
flex: none;
|
|
374
|
+
width: 16px;
|
|
375
|
+
display: inline-flex;
|
|
376
|
+
align-items: center;
|
|
377
|
+
justify-content: center;
|
|
378
|
+
font-size: 13px;
|
|
379
|
+
line-height: 1;
|
|
380
|
+
}
|
|
381
|
+
|
|
340
382
|
.tabClose {
|
|
341
383
|
appearance: none;
|
|
342
384
|
border: none;
|
|
@@ -378,6 +420,29 @@
|
|
|
378
420
|
position: relative;
|
|
379
421
|
height: 100%;
|
|
380
422
|
min-height: 0;
|
|
423
|
+
/* Keep layout/paint churn local to the editor: huge wrapped content must
|
|
424
|
+
not invalidate the whole page on every edit or scroll. */
|
|
425
|
+
contain: layout paint;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/* Plain (no-overlay) editor: the textarea itself renders the visible text —
|
|
429
|
+
used for files without syntax highlighting (e.g. .txt novels) and for
|
|
430
|
+
files too large to re-highlight per keystroke without lag. */
|
|
431
|
+
.editorPlain .editorTextarea {
|
|
432
|
+
color: var(--fe-fg);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/* Large-file notice chip: syntax highlight was dropped to stay responsive. */
|
|
436
|
+
.editorHint {
|
|
437
|
+
position: absolute;
|
|
438
|
+
left: 8px;
|
|
439
|
+
bottom: 8px;
|
|
440
|
+
padding: 4px 10px;
|
|
441
|
+
border-radius: 4px;
|
|
442
|
+
background: var(--fe-hover);
|
|
443
|
+
color: var(--fe-muted);
|
|
444
|
+
font-size: 11px;
|
|
445
|
+
pointer-events: none;
|
|
381
446
|
}
|
|
382
447
|
|
|
383
448
|
.editorHighlight {
|
|
@@ -385,12 +450,22 @@
|
|
|
385
450
|
inset: 0;
|
|
386
451
|
margin: 0;
|
|
387
452
|
padding: 12px;
|
|
388
|
-
|
|
453
|
+
/* The textarea reserves one extra line at the bottom (its caret reserve),
|
|
454
|
+
so it can scroll ~1 line further than this layer. Match that range so
|
|
455
|
+
the two layers never desync at the bottom of a long file — otherwise
|
|
456
|
+
drag-selection near the end would highlight text one line ABOVE the
|
|
457
|
+
visible text. */
|
|
458
|
+
padding-bottom: calc(12px + 1.5em);
|
|
459
|
+
/* auto (not hidden): the highlight layer gets the same scrollbar gutter as
|
|
460
|
+
the textarea, so both content boxes have identical widths and wrap at the
|
|
461
|
+
same points. Its scrollbars stack invisibly beneath the textarea's. */
|
|
462
|
+
overflow: auto;
|
|
389
463
|
pointer-events: none;
|
|
390
464
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
391
465
|
font-size: 13px;
|
|
392
466
|
line-height: 1.5;
|
|
393
467
|
white-space: pre;
|
|
468
|
+
tab-size: 2;
|
|
394
469
|
color: var(--fe-fg);
|
|
395
470
|
background: transparent;
|
|
396
471
|
}
|
|
@@ -422,6 +497,18 @@
|
|
|
422
497
|
tab-size: 2;
|
|
423
498
|
}
|
|
424
499
|
|
|
500
|
+
/* Display-only soft wrap: both layers wrap identically; the file content
|
|
501
|
+
itself is never modified (wrap="soft" on the textarea). */
|
|
502
|
+
.editor[data-wrap='on'] .editorHighlight,
|
|
503
|
+
.editor[data-wrap='on'] .editorTextarea {
|
|
504
|
+
white-space: pre-wrap;
|
|
505
|
+
overflow-wrap: anywhere;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.editor[data-wrap='on'] .editorTextarea {
|
|
509
|
+
overflow-x: hidden;
|
|
510
|
+
}
|
|
511
|
+
|
|
425
512
|
.saveError {
|
|
426
513
|
position: absolute;
|
|
427
514
|
right: 8px;
|
|
@@ -515,8 +602,13 @@
|
|
|
515
602
|
font-style: italic;
|
|
516
603
|
}
|
|
517
604
|
|
|
605
|
+
/* Strong emphasis is color-only on purpose: `font-weight: bold` widens
|
|
606
|
+
glyphs, so the highlight layer would wrap at different points than the
|
|
607
|
+
textarea beneath it — drag-selection would then highlight text that no
|
|
608
|
+
longer matches the visible lines. Italic above is metric-neutral. */
|
|
518
609
|
.editorHighlight :global(.hljs-strong) {
|
|
519
|
-
|
|
610
|
+
color: var(--fe-accent);
|
|
611
|
+
font-weight: inherit;
|
|
520
612
|
}
|
|
521
613
|
|
|
522
614
|
/* ---- Skin chrome fix ----
|
package/src/client/locales.ts
CHANGED
|
@@ -9,6 +9,8 @@ export const zh = {
|
|
|
9
9
|
'action.open': '在系统中打开',
|
|
10
10
|
'action.theme': '切换浅色/暗色主题',
|
|
11
11
|
'action.saveHint': 'Ctrl+S 保存',
|
|
12
|
+
'action.wrapOn': '开启自动换行(显示层换行,不改文件)',
|
|
13
|
+
'action.wrapOff': '关闭自动换行(长行横向滚动)',
|
|
12
14
|
'tab.close': '关闭标签页',
|
|
13
15
|
'tab.collapse': '收起文件详情',
|
|
14
16
|
'tab.expand': '弹出文件详情',
|
|
@@ -17,6 +19,7 @@ export const zh = {
|
|
|
17
19
|
'preview.tooLarge': '文件过大,仅支持预览不超过 {limit}',
|
|
18
20
|
'preview.emptyDir': '空目录',
|
|
19
21
|
'preview.error': '加载失败',
|
|
22
|
+
'preview.highlightOff': '大文件:已关闭语法高亮(仍可编辑)',
|
|
20
23
|
} as const
|
|
21
24
|
|
|
22
25
|
export type FilesKey = keyof typeof zh
|
|
@@ -28,6 +31,8 @@ export const en: Record<FilesKey, string> = {
|
|
|
28
31
|
'action.open': 'Open in system',
|
|
29
32
|
'action.theme': 'Toggle light/dark theme',
|
|
30
33
|
'action.saveHint': 'Ctrl+S to save',
|
|
34
|
+
'action.wrapOn': 'Enable word wrap (display only, file unchanged)',
|
|
35
|
+
'action.wrapOff': 'Disable word wrap (long lines scroll horizontally)',
|
|
31
36
|
'tab.close': 'Close tab',
|
|
32
37
|
'tab.collapse': 'Collapse file details',
|
|
33
38
|
'tab.expand': 'Expand file details',
|
|
@@ -36,4 +41,5 @@ export const en: Record<FilesKey, string> = {
|
|
|
36
41
|
'preview.tooLarge': 'File too large to preview (limit {limit})',
|
|
37
42
|
'preview.emptyDir': 'Empty directory',
|
|
38
43
|
'preview.error': 'Load failed',
|
|
44
|
+
'preview.highlightOff': 'Large file: syntax highlighting off (still editable)',
|
|
39
45
|
}
|
package/src/client/store.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface TabsStateView {
|
|
|
20
20
|
tabs: string[]
|
|
21
21
|
active: string | undefined
|
|
22
22
|
collapsed: boolean
|
|
23
|
+
/** Display-only soft wrap for the editor (never touches the file content). */
|
|
24
|
+
wrap: boolean
|
|
23
25
|
theme: FeTheme
|
|
24
26
|
cwd: string | undefined
|
|
25
27
|
}
|
|
@@ -27,14 +29,29 @@ export interface TabsStateView {
|
|
|
27
29
|
const EMPTY_TABS: string[] = []
|
|
28
30
|
const EMPTY_WORKSPACE: PerWorkspace = { tabs: EMPTY_TABS, active: undefined, collapsed: false }
|
|
29
31
|
|
|
32
|
+
const WRAP_KEY = 'dsh-plugin-workbench:wrap'
|
|
33
|
+
|
|
34
|
+
/** Read the persisted wrap preference; defaults to soft wrap ON. */
|
|
35
|
+
function storedWrap(): boolean {
|
|
36
|
+
try {
|
|
37
|
+
if (typeof window === 'undefined') return true
|
|
38
|
+
const stored = window.localStorage.getItem(WRAP_KEY)
|
|
39
|
+
return stored === null ? true : stored === '1'
|
|
40
|
+
} catch {
|
|
41
|
+
return true
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
30
45
|
interface StoreState {
|
|
31
46
|
currentCwd: string | undefined
|
|
32
47
|
workspaces: Record<string, PerWorkspace>
|
|
33
48
|
theme: FeTheme
|
|
49
|
+
wrap: boolean
|
|
34
50
|
}
|
|
35
51
|
|
|
36
|
-
|
|
37
|
-
let
|
|
52
|
+
const initialWrap = storedWrap()
|
|
53
|
+
let state: StoreState = { currentCwd: undefined, workspaces: {}, theme: 'dark', wrap: initialWrap }
|
|
54
|
+
let view: TabsStateView = { tabs: EMPTY_TABS, active: undefined, collapsed: false, wrap: initialWrap, theme: 'dark', cwd: undefined }
|
|
38
55
|
const listeners = new Set<() => void>()
|
|
39
56
|
|
|
40
57
|
function workspaceOf(cwd: string | undefined): PerWorkspace {
|
|
@@ -63,6 +80,7 @@ function commit(next: StoreState): void {
|
|
|
63
80
|
tabs: ws.tabs,
|
|
64
81
|
active: ws.active,
|
|
65
82
|
collapsed: ws.collapsed,
|
|
83
|
+
wrap: state.wrap,
|
|
66
84
|
theme: state.theme,
|
|
67
85
|
cwd: state.currentCwd,
|
|
68
86
|
}
|
|
@@ -70,6 +88,7 @@ function commit(next: StoreState): void {
|
|
|
70
88
|
nextView.tabs === view.tabs
|
|
71
89
|
&& nextView.active === view.active
|
|
72
90
|
&& nextView.collapsed === view.collapsed
|
|
91
|
+
&& nextView.wrap === view.wrap
|
|
73
92
|
&& nextView.theme === view.theme
|
|
74
93
|
&& nextView.cwd === view.cwd
|
|
75
94
|
) return
|
|
@@ -149,3 +168,14 @@ export function expandPreview(): void {
|
|
|
149
168
|
export function toggleTheme(): void {
|
|
150
169
|
commit({ ...state, theme: state.theme === 'dark' ? 'light' : 'dark' })
|
|
151
170
|
}
|
|
171
|
+
|
|
172
|
+
/** Toggle display-only soft wrap for the editor (global, persisted). */
|
|
173
|
+
export function toggleWrap(): void {
|
|
174
|
+
const wrap = !state.wrap
|
|
175
|
+
try {
|
|
176
|
+
window.localStorage.setItem(WRAP_KEY, wrap ? '1' : '0')
|
|
177
|
+
} catch {
|
|
178
|
+
// storage unavailable — the preference just won't persist across reloads
|
|
179
|
+
}
|
|
180
|
+
commit({ ...state, wrap })
|
|
181
|
+
}
|