dsh-plugin-workbench 0.0.4 → 0.0.6
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 +65 -2
- package/README.md +29 -7
- package/lib/client.js +6770 -117
- package/lib/client.js.map +1 -1
- package/lib/index.js +467 -7
- package/package.json +2 -1
- package/src/client/FileExplorer.tsx +740 -13
- package/src/client/FilePreview.tsx +279 -17
- package/src/client/files.module.css +389 -3
- package/src/client/highlight.ts +18 -1
- package/src/client/index.ts +27 -4
- package/src/client/locales.ts +72 -0
- package/src/client/markdown.ts +69 -0
- package/src/client/store.ts +157 -2
- package/src/dsh.d.ts +9 -0
- package/src/index.ts +601 -10
|
@@ -128,6 +128,12 @@
|
|
|
128
128
|
padding: 4px;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
/* Keyboard focus lands on the tree container (rows push focus here so
|
|
132
|
+
Ctrl+C / Ctrl+V work right after a click); no visible focus ring. */
|
|
133
|
+
.treeArea:focus {
|
|
134
|
+
outline: none;
|
|
135
|
+
}
|
|
136
|
+
|
|
131
137
|
.row {
|
|
132
138
|
display: flex;
|
|
133
139
|
align-items: center;
|
|
@@ -153,6 +159,42 @@
|
|
|
153
159
|
color: var(--fe-fg);
|
|
154
160
|
}
|
|
155
161
|
|
|
162
|
+
/* Cut items stay visible but ghosted until the paste lands (OS explorer look). */
|
|
163
|
+
.rowCut {
|
|
164
|
+
opacity: 0.45;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* A directory row that will receive a drag-drop move gets an accent outline. */
|
|
168
|
+
.rowDropTarget {
|
|
169
|
+
box-shadow: inset 0 0 0 1.5px var(--fe-accent);
|
|
170
|
+
background: var(--fe-selection);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* ---- Clipboard status bar (Copy / Cut + Paste) ---- */
|
|
174
|
+
.clipboardBar {
|
|
175
|
+
flex: none;
|
|
176
|
+
display: flex;
|
|
177
|
+
align-items: center;
|
|
178
|
+
gap: 6px;
|
|
179
|
+
padding: 4px 8px;
|
|
180
|
+
border-bottom: 1px solid var(--fe-border);
|
|
181
|
+
background: var(--fe-hover);
|
|
182
|
+
color: var(--fe-fg);
|
|
183
|
+
font-size: 12px;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.clipboardText {
|
|
187
|
+
flex: 1;
|
|
188
|
+
min-width: 0;
|
|
189
|
+
overflow: hidden;
|
|
190
|
+
text-overflow: ellipsis;
|
|
191
|
+
white-space: nowrap;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.clipboardHint {
|
|
195
|
+
color: var(--fe-faint);
|
|
196
|
+
}
|
|
197
|
+
|
|
156
198
|
.chevron {
|
|
157
199
|
flex: none;
|
|
158
200
|
width: 12px;
|
|
@@ -235,6 +277,60 @@
|
|
|
235
277
|
padding: 2px 8px 2px 0;
|
|
236
278
|
}
|
|
237
279
|
|
|
280
|
+
/* ---- Right-click context menu (VS Code-style) ---- */
|
|
281
|
+
/* The maid-atelier skin styles every [role='menu'] with its own palette —
|
|
282
|
+
`color` follows the PAGE theme there, while our background follows the
|
|
283
|
+
WORKBENCH theme. When the two disagree (page dark + workbench light or
|
|
284
|
+
vice versa) the skin's higher-specificity color would paint menu text in a
|
|
285
|
+
shade that matches our background, making it unreadable. Lock the menu's
|
|
286
|
+
own colors to the workbench tokens so text always contrasts its bg. */
|
|
287
|
+
.contextMenu {
|
|
288
|
+
position: fixed;
|
|
289
|
+
z-index: 10000;
|
|
290
|
+
min-width: 180px;
|
|
291
|
+
padding: 4px;
|
|
292
|
+
box-sizing: border-box;
|
|
293
|
+
border: 1px solid var(--fe-border-strong);
|
|
294
|
+
border-radius: 6px;
|
|
295
|
+
background: var(--fe-bg) !important;
|
|
296
|
+
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.28);
|
|
297
|
+
font-size: 13px;
|
|
298
|
+
color: var(--fe-fg) !important;
|
|
299
|
+
user-select: none;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.contextMenuItem {
|
|
303
|
+
display: flex;
|
|
304
|
+
align-items: center;
|
|
305
|
+
gap: 8px;
|
|
306
|
+
padding: 5px 10px;
|
|
307
|
+
border-radius: 4px;
|
|
308
|
+
cursor: pointer;
|
|
309
|
+
white-space: nowrap;
|
|
310
|
+
color: inherit;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.contextMenuItem:hover {
|
|
314
|
+
background: var(--fe-selection) !important;
|
|
315
|
+
color: var(--fe-fg) !important;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.contextMenuItemDanger {
|
|
319
|
+
color: #f48771 !important;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.contextMenuItemDisabled {
|
|
323
|
+
opacity: 0.45;
|
|
324
|
+
cursor: default;
|
|
325
|
+
pointer-events: none;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.contextMenuDivider {
|
|
329
|
+
height: 1px;
|
|
330
|
+
margin: 4px 6px;
|
|
331
|
+
background: var(--fe-border);
|
|
332
|
+
}
|
|
333
|
+
|
|
238
334
|
/* ---- Split preview + tabs ---- */
|
|
239
335
|
.preview {
|
|
240
336
|
flex: 0 0 55%;
|
|
@@ -409,6 +505,31 @@
|
|
|
409
505
|
opacity: 0.85;
|
|
410
506
|
}
|
|
411
507
|
|
|
508
|
+
/* Disk-changed badge: the file changed on disk while this tab holds unsaved
|
|
509
|
+
edits; click to reload and discard them. */
|
|
510
|
+
.tabDiskBadge {
|
|
511
|
+
flex: none;
|
|
512
|
+
appearance: none;
|
|
513
|
+
border: none;
|
|
514
|
+
background: transparent;
|
|
515
|
+
color: var(--fe-accent);
|
|
516
|
+
cursor: pointer;
|
|
517
|
+
font-size: 12px;
|
|
518
|
+
line-height: 1;
|
|
519
|
+
width: 16px;
|
|
520
|
+
height: 16px;
|
|
521
|
+
border-radius: 3px;
|
|
522
|
+
display: inline-flex;
|
|
523
|
+
align-items: center;
|
|
524
|
+
justify-content: center;
|
|
525
|
+
padding: 0;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.tabDiskBadge:hover {
|
|
529
|
+
background: var(--fe-hover);
|
|
530
|
+
color: var(--fe-fg);
|
|
531
|
+
}
|
|
532
|
+
|
|
412
533
|
.previewBody {
|
|
413
534
|
flex: 1;
|
|
414
535
|
min-height: 0;
|
|
@@ -420,11 +541,39 @@
|
|
|
420
541
|
position: relative;
|
|
421
542
|
height: 100%;
|
|
422
543
|
min-height: 0;
|
|
544
|
+
/* Line-number gutter width; the textarea and highlight layers shift past it. */
|
|
545
|
+
--fe-gutter: 44px;
|
|
423
546
|
/* Keep layout/paint churn local to the editor: huge wrapped content must
|
|
424
547
|
not invalidate the whole page on every edit or scroll. */
|
|
425
548
|
contain: layout paint;
|
|
426
549
|
}
|
|
427
550
|
|
|
551
|
+
/* Line-number gutter: a fixed column on the left that scrolls in lockstep
|
|
552
|
+
with the editor text (scrollTop synced from the textarea every frame). */
|
|
553
|
+
.gutter {
|
|
554
|
+
position: absolute;
|
|
555
|
+
top: 0;
|
|
556
|
+
bottom: 0;
|
|
557
|
+
left: 0;
|
|
558
|
+
width: var(--fe-gutter);
|
|
559
|
+
box-sizing: border-box;
|
|
560
|
+
overflow: hidden;
|
|
561
|
+
border-right: 1px solid var(--fe-border);
|
|
562
|
+
background: var(--fe-sidebar-bg);
|
|
563
|
+
color: var(--fe-faint);
|
|
564
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
565
|
+
font-size: 13px;
|
|
566
|
+
line-height: 1.5;
|
|
567
|
+
user-select: none;
|
|
568
|
+
pointer-events: none;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.gutterNumbers {
|
|
572
|
+
padding: 12px 8px;
|
|
573
|
+
white-space: pre;
|
|
574
|
+
text-align: right;
|
|
575
|
+
}
|
|
576
|
+
|
|
428
577
|
/* Plain (no-overlay) editor: the textarea itself renders the visible text —
|
|
429
578
|
used for files without syntax highlighting (e.g. .txt novels) and for
|
|
430
579
|
files too large to re-highlight per keystroke without lag. */
|
|
@@ -435,7 +584,7 @@
|
|
|
435
584
|
/* Large-file notice chip: syntax highlight was dropped to stay responsive. */
|
|
436
585
|
.editorHint {
|
|
437
586
|
position: absolute;
|
|
438
|
-
left: 8px;
|
|
587
|
+
left: calc(var(--fe-gutter) + 8px);
|
|
439
588
|
bottom: 8px;
|
|
440
589
|
padding: 4px 10px;
|
|
441
590
|
border-radius: 4px;
|
|
@@ -447,7 +596,10 @@
|
|
|
447
596
|
|
|
448
597
|
.editorHighlight {
|
|
449
598
|
position: absolute;
|
|
450
|
-
|
|
599
|
+
top: 0;
|
|
600
|
+
bottom: 0;
|
|
601
|
+
left: var(--fe-gutter);
|
|
602
|
+
right: 0;
|
|
451
603
|
margin: 0;
|
|
452
604
|
padding: 12px;
|
|
453
605
|
/* The textarea reserves one extra line at the bottom (its caret reserve),
|
|
@@ -479,7 +631,10 @@
|
|
|
479
631
|
|
|
480
632
|
.editorTextarea {
|
|
481
633
|
position: absolute;
|
|
482
|
-
|
|
634
|
+
top: 0;
|
|
635
|
+
bottom: 0;
|
|
636
|
+
left: var(--fe-gutter);
|
|
637
|
+
right: 0;
|
|
483
638
|
box-sizing: border-box;
|
|
484
639
|
margin: 0;
|
|
485
640
|
padding: 12px;
|
|
@@ -527,11 +682,242 @@
|
|
|
527
682
|
line-height: 18px;
|
|
528
683
|
}
|
|
529
684
|
|
|
685
|
+
/* Image preview: raw bytes arrive from the plugin's same-origin route. The
|
|
686
|
+
viewer centers the picture and shrinks it to fit the pane, letting the
|
|
687
|
+
body scroll when the file is larger than the available space. */
|
|
688
|
+
.imageView {
|
|
689
|
+
height: 100%;
|
|
690
|
+
min-height: 0;
|
|
691
|
+
box-sizing: border-box;
|
|
692
|
+
display: flex;
|
|
693
|
+
flex-direction: column;
|
|
694
|
+
align-items: center;
|
|
695
|
+
justify-content: center;
|
|
696
|
+
gap: 8px;
|
|
697
|
+
padding: 12px;
|
|
698
|
+
overflow: auto;
|
|
699
|
+
background: var(--fe-bg);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
.image {
|
|
703
|
+
max-width: 100%;
|
|
704
|
+
max-height: 100%;
|
|
705
|
+
object-fit: contain;
|
|
706
|
+
border-radius: 4px;
|
|
707
|
+
box-shadow: 0 0 0 1px var(--fe-border);
|
|
708
|
+
}
|
|
709
|
+
|
|
530
710
|
.previewMeta {
|
|
531
711
|
color: var(--fe-faint);
|
|
532
712
|
font-size: 11px;
|
|
533
713
|
}
|
|
534
714
|
|
|
715
|
+
/* ---- Markdown rendered preview ---- */
|
|
716
|
+
.mdPreview {
|
|
717
|
+
box-sizing: border-box;
|
|
718
|
+
height: 100%;
|
|
719
|
+
overflow: auto;
|
|
720
|
+
padding: 16px 20px 48px;
|
|
721
|
+
color: var(--fe-fg);
|
|
722
|
+
font-size: 14px;
|
|
723
|
+
line-height: 1.7;
|
|
724
|
+
word-wrap: break-word;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
.mdPreview > :first-child {
|
|
728
|
+
margin-top: 0;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
.mdPreview > :last-child {
|
|
732
|
+
margin-bottom: 0;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
.mdPreview h1,
|
|
736
|
+
.mdPreview h2,
|
|
737
|
+
.mdPreview h3,
|
|
738
|
+
.mdPreview h4,
|
|
739
|
+
.mdPreview h5,
|
|
740
|
+
.mdPreview h6 {
|
|
741
|
+
margin: 1.2em 0 0.5em;
|
|
742
|
+
line-height: 1.3;
|
|
743
|
+
font-weight: 600;
|
|
744
|
+
color: var(--fe-fg);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.mdPreview h1 {
|
|
748
|
+
font-size: 1.7em;
|
|
749
|
+
padding-bottom: 0.3em;
|
|
750
|
+
border-bottom: 1px solid var(--fe-border);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
.mdPreview h2 {
|
|
754
|
+
font-size: 1.4em;
|
|
755
|
+
padding-bottom: 0.3em;
|
|
756
|
+
border-bottom: 1px solid var(--fe-border);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
.mdPreview h3 {
|
|
760
|
+
font-size: 1.2em;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
.mdPreview h4,
|
|
764
|
+
.mdPreview h5,
|
|
765
|
+
.mdPreview h6 {
|
|
766
|
+
font-size: 1.05em;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
.mdPreview p {
|
|
770
|
+
margin: 0.6em 0;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
.mdPreview ul,
|
|
774
|
+
.mdPreview ol {
|
|
775
|
+
margin: 0.6em 0;
|
|
776
|
+
padding-left: 1.6em;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
.mdPreview li {
|
|
780
|
+
margin: 0.2em 0;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
.mdPreview li > ul,
|
|
784
|
+
.mdPreview li > ol {
|
|
785
|
+
margin: 0.2em 0;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
.mdPreview a {
|
|
789
|
+
color: var(--fe-accent);
|
|
790
|
+
text-decoration: none;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
.mdPreview a:hover {
|
|
794
|
+
text-decoration: underline;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
.mdPreview blockquote {
|
|
798
|
+
margin: 0.8em 0;
|
|
799
|
+
padding: 0.3em 1em;
|
|
800
|
+
border-left: 3px solid var(--fe-border-strong);
|
|
801
|
+
color: var(--fe-muted);
|
|
802
|
+
background: var(--fe-sidebar-bg);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
.mdPreview code {
|
|
806
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
807
|
+
font-size: 0.92em;
|
|
808
|
+
background: var(--fe-hover);
|
|
809
|
+
padding: 0.15em 0.4em;
|
|
810
|
+
border-radius: 3px;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
.mdPreview pre {
|
|
814
|
+
margin: 0.8em 0;
|
|
815
|
+
padding: 12px;
|
|
816
|
+
overflow: auto;
|
|
817
|
+
background: var(--fe-sidebar-bg);
|
|
818
|
+
border: 1px solid var(--fe-border);
|
|
819
|
+
border-radius: 4px;
|
|
820
|
+
line-height: 1.5;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
.mdPreview pre code {
|
|
824
|
+
background: transparent;
|
|
825
|
+
padding: 0;
|
|
826
|
+
font-size: 13px;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
.mdPreview table {
|
|
830
|
+
border-collapse: collapse;
|
|
831
|
+
margin: 0.8em 0;
|
|
832
|
+
display: block;
|
|
833
|
+
max-width: 100%;
|
|
834
|
+
overflow-x: auto;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
.mdPreview th,
|
|
838
|
+
.mdPreview td {
|
|
839
|
+
border: 1px solid var(--fe-border-strong);
|
|
840
|
+
padding: 6px 12px;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
.mdPreview th {
|
|
844
|
+
background: var(--fe-sidebar-bg);
|
|
845
|
+
font-weight: 600;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
.mdPreview img {
|
|
849
|
+
max-width: 100%;
|
|
850
|
+
border-radius: 4px;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
.mdPreview hr {
|
|
854
|
+
border: none;
|
|
855
|
+
border-top: 1px solid var(--fe-border-strong);
|
|
856
|
+
margin: 1.5em 0;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/* Fenced code blocks reuse the same token palette as the overlay editor. */
|
|
860
|
+
.mdPreview :global(.hljs-keyword),
|
|
861
|
+
.mdPreview :global(.hljs-selector-tag),
|
|
862
|
+
.mdPreview :global(.hljs-literal) {
|
|
863
|
+
color: var(--fe-token-keyword);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
.mdPreview :global(.hljs-string),
|
|
867
|
+
.mdPreview :global(.hljs-regexp),
|
|
868
|
+
.mdPreview :global(.hljs-addition) {
|
|
869
|
+
color: var(--fe-token-string);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
.mdPreview :global(.hljs-comment),
|
|
873
|
+
.mdPreview :global(.hljs-quote) {
|
|
874
|
+
color: var(--fe-token-comment);
|
|
875
|
+
font-style: italic;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
.mdPreview :global(.hljs-number),
|
|
879
|
+
.mdPreview :global(.hljs-symbol),
|
|
880
|
+
.mdPreview :global(.hljs-bullet) {
|
|
881
|
+
color: var(--fe-token-number);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
.mdPreview :global(.hljs-title),
|
|
885
|
+
.mdPreview :global(.hljs-section),
|
|
886
|
+
.mdPreview :global(.hljs-title.function_) {
|
|
887
|
+
color: var(--fe-token-function);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
.mdPreview :global(.hljs-type),
|
|
891
|
+
.mdPreview :global(.hljs-built_in),
|
|
892
|
+
.mdPreview :global(.hljs-class .hljs-title) {
|
|
893
|
+
color: var(--fe-token-type);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
.mdPreview :global(.hljs-attr),
|
|
897
|
+
.mdPreview :global(.hljs-attribute),
|
|
898
|
+
.mdPreview :global(.hljs-variable),
|
|
899
|
+
.mdPreview :global(.hljs-template-variable),
|
|
900
|
+
.mdPreview :global(.hljs-params) {
|
|
901
|
+
color: var(--fe-token-variable);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
.mdPreview :global(.hljs-tag),
|
|
905
|
+
.mdPreview :global(.hljs-name) {
|
|
906
|
+
color: var(--fe-token-tag);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
.mdPreview :global(.hljs-meta) {
|
|
910
|
+
color: var(--fe-token-meta);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
.mdPreview :global(.hljs-emphasis) {
|
|
914
|
+
font-style: italic;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
.mdPreview :global(.hljs-strong) {
|
|
918
|
+
font-weight: 600;
|
|
919
|
+
}
|
|
920
|
+
|
|
535
921
|
.placeholder {
|
|
536
922
|
flex: 1;
|
|
537
923
|
display: flex;
|
package/src/client/highlight.ts
CHANGED
|
@@ -65,7 +65,7 @@ export function detectLanguage(path: string): string | undefined {
|
|
|
65
65
|
return EXTENSION_TO_LANGUAGE[ext]
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
function escapeHtml(value: string): string {
|
|
68
|
+
export function escapeHtml(value: string): string {
|
|
69
69
|
return value
|
|
70
70
|
.replace(/&/g, '&')
|
|
71
71
|
.replace(/</g, '<')
|
|
@@ -74,6 +74,11 @@ function escapeHtml(value: string): string {
|
|
|
74
74
|
.replace(/'/g, ''')
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/** True when a language id names a registered highlight.js language. */
|
|
78
|
+
export function canHighlight(language: string): boolean {
|
|
79
|
+
return language !== '' && hljs.getLanguage(language) !== undefined
|
|
80
|
+
}
|
|
81
|
+
|
|
77
82
|
/** Highlight code for a file; returns safe HTML (code is HTML-escaped). */
|
|
78
83
|
export function highlightCode(code: string, path: string): string {
|
|
79
84
|
const language = detectLanguage(path)
|
|
@@ -86,3 +91,15 @@ export function highlightCode(code: string, path: string): string {
|
|
|
86
91
|
}
|
|
87
92
|
return escapeHtml(code)
|
|
88
93
|
}
|
|
94
|
+
|
|
95
|
+
/** Highlight a fenced code block by its declared language; escaped plain text when unknown. */
|
|
96
|
+
export function highlightFence(code: string, language: string): string {
|
|
97
|
+
if (canHighlight(language)) {
|
|
98
|
+
try {
|
|
99
|
+
return hljs.highlight(code, { language, ignoreIllegals: true }).value
|
|
100
|
+
} catch {
|
|
101
|
+
// fall through to escaped text
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return escapeHtml(code)
|
|
105
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -23,23 +23,46 @@ export function apply(ctx: Context): void {
|
|
|
23
23
|
const listDir = (path: string, signal?: AbortSignal) => unwrap(ctx.connection.rpc.call(CHANNEL, 'list', { path }, signal))
|
|
24
24
|
const readFile = (path: string, signal?: AbortSignal) => unwrap(ctx.connection.rpc.call(CHANNEL, 'read', { path }, signal))
|
|
25
25
|
const writeFile = (path: string, content: string, signal?: AbortSignal) => unwrap(ctx.connection.rpc.call(CHANNEL, 'write', { path, content }, signal))
|
|
26
|
+
const watchFiles = (paths: string[]) => unwrap(ctx.connection.rpc.call(CHANNEL, 'watch', { paths }))
|
|
27
|
+
const createFile = (path: string, signal?: AbortSignal) => unwrap(ctx.connection.rpc.call(CHANNEL, 'createFile', { path }, signal))
|
|
28
|
+
const createDir = (path: string, signal?: AbortSignal) => unwrap(ctx.connection.rpc.call(CHANNEL, 'createDir', { path }, signal))
|
|
29
|
+
const renameFile = (path: string, to: string, signal?: AbortSignal) => unwrap(ctx.connection.rpc.call(CHANNEL, 'rename', { path, to }, signal))
|
|
30
|
+
const removePath = (path: string, signal?: AbortSignal) => unwrap(ctx.connection.rpc.call(CHANNEL, 'delete', { path }, signal))
|
|
31
|
+
const copyPath = (from: string, to: string, overwrite: boolean, signal?: AbortSignal) =>
|
|
32
|
+
unwrap(ctx.connection.rpc.call(CHANNEL, 'copy', { from, to, overwrite }, signal))
|
|
33
|
+
const revealInExplorer = (path: string, kind: 'file' | 'dir', signal?: AbortSignal) =>
|
|
34
|
+
unwrap(ctx.connection.rpc.call(CHANNEL, 'reveal', { path, kind }, signal))
|
|
26
35
|
const openPath = (path: string) => ctx.workspaces.openPath(path)
|
|
27
36
|
|
|
28
37
|
ctx.slots.inject('explorer', () => ctx.slots.register({
|
|
29
38
|
name: 'explorer',
|
|
30
39
|
locale: NS,
|
|
31
|
-
inject: () => ({ listDir, openPath }),
|
|
40
|
+
inject: () => ({ listDir, openPath, revealInExplorer, createFile, createDir, renameFile, removePath, copyPath }),
|
|
32
41
|
}, FileExplorer))
|
|
33
42
|
|
|
34
43
|
ctx.slots.inject('explorer.preview', () => ctx.slots.register({
|
|
35
44
|
name: 'explorer.preview',
|
|
36
45
|
locale: NS,
|
|
37
|
-
inject: () => ({ readFile, writeFile }),
|
|
46
|
+
inject: () => ({ readFile, writeFile, watchFiles }),
|
|
38
47
|
}, FilePreview))
|
|
39
48
|
}
|
|
40
49
|
|
|
41
|
-
|
|
50
|
+
interface RpcFailure {
|
|
51
|
+
ok: false
|
|
52
|
+
error: { code: string; message: string }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Error carrying the host's machine-readable `code` (e.g. 'exists' on copy collision). */
|
|
56
|
+
export interface RpcError extends Error {
|
|
57
|
+
code?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function unwrap<T>(result: Promise<{ ok: true; value: T } | RpcFailure>): Promise<T> {
|
|
42
61
|
const resolved = await result
|
|
43
|
-
if (!resolved.ok)
|
|
62
|
+
if (!resolved.ok) {
|
|
63
|
+
const error = new Error(resolved.error.message) as RpcError
|
|
64
|
+
error.code = resolved.error.code
|
|
65
|
+
throw error
|
|
66
|
+
}
|
|
44
67
|
return resolved.value
|
|
45
68
|
}
|
package/src/client/locales.ts
CHANGED
|
@@ -11,15 +11,51 @@ export const zh = {
|
|
|
11
11
|
'action.saveHint': 'Ctrl+S 保存',
|
|
12
12
|
'action.wrapOn': '开启自动换行(显示层换行,不改文件)',
|
|
13
13
|
'action.wrapOff': '关闭自动换行(长行横向滚动)',
|
|
14
|
+
'action.mdRender': '渲染预览(Markdown)',
|
|
15
|
+
'action.mdSource': '查看 Markdown 源码',
|
|
14
16
|
'tab.close': '关闭标签页',
|
|
15
17
|
'tab.collapse': '收起文件详情',
|
|
16
18
|
'tab.expand': '弹出文件详情',
|
|
19
|
+
'tab.diskChanged': '文件已在磁盘上被修改,点击重新加载(会放弃未保存的编辑)',
|
|
20
|
+
'menu.open': '打开预览',
|
|
21
|
+
'menu.newFile': '新建文件',
|
|
22
|
+
'menu.newFolder': '新建文件夹',
|
|
23
|
+
'menu.rename': '重命名',
|
|
24
|
+
'menu.delete': '删除',
|
|
25
|
+
'menu.copyPath': '复制路径',
|
|
26
|
+
'menu.copy': '复制',
|
|
27
|
+
'menu.cut': '剪切',
|
|
28
|
+
'menu.paste': '粘贴',
|
|
29
|
+
'menu.refresh': '刷新',
|
|
30
|
+
'menu.openSystem': '在系统中打开',
|
|
31
|
+
'menu.revealInExplorer': '在资源管理器打开',
|
|
32
|
+
'menu.deleteSelected': '删除选中的 {count} 项',
|
|
33
|
+
'action.undo': '撤销(Ctrl+Z)',
|
|
34
|
+
'undo.copy': '复制「{name}」',
|
|
35
|
+
'undo.move': '移动「{name}」',
|
|
36
|
+
'undo.rename': '重命名「{name}」',
|
|
37
|
+
'undo.create': '新建「{name}」',
|
|
38
|
+
'undo.delete': '删除「{name}」',
|
|
39
|
+
'error.reveal': '在资源管理器打开失败',
|
|
40
|
+
'prompt.newFileName': '新文件名',
|
|
41
|
+
'prompt.newFolderName': '新文件夹名',
|
|
42
|
+
'prompt.renameTo': '重命名为',
|
|
43
|
+
'confirm.deleteFile': '确定删除文件「{name}」吗?(可撤销)',
|
|
44
|
+
'confirm.deleteDir': '确定删除文件夹「{name}」及其全部内容吗?(可撤销)',
|
|
45
|
+
'confirm.deleteSelected': '确定删除选中的 {count} 项吗?{names}(可撤销)',
|
|
46
|
+
'confirm.overwrite': '「{name}」已存在,是否覆盖?',
|
|
47
|
+
'clipboard.copied': '已复制 {count} 项',
|
|
48
|
+
'clipboard.cut': '已剪切 {count} 项',
|
|
49
|
+
'clipboard.pasteHint': ' · Ctrl+V 粘贴到当前目录',
|
|
50
|
+
'clipboard.clear': '清除剪贴板',
|
|
17
51
|
'preview.loading': '加载中…',
|
|
18
52
|
'preview.binary': '二进制文件,无法预览',
|
|
53
|
+
'preview.imageFailed': '图片加载失败',
|
|
19
54
|
'preview.tooLarge': '文件过大,仅支持预览不超过 {limit}',
|
|
20
55
|
'preview.emptyDir': '空目录',
|
|
21
56
|
'preview.error': '加载失败',
|
|
22
57
|
'preview.highlightOff': '大文件:已关闭语法高亮(仍可编辑)',
|
|
58
|
+
'preview.mdRenderOff': '大文件:已切换为源码视图(仍可编辑)',
|
|
23
59
|
} as const
|
|
24
60
|
|
|
25
61
|
export type FilesKey = keyof typeof zh
|
|
@@ -33,13 +69,49 @@ export const en: Record<FilesKey, string> = {
|
|
|
33
69
|
'action.saveHint': 'Ctrl+S to save',
|
|
34
70
|
'action.wrapOn': 'Enable word wrap (display only, file unchanged)',
|
|
35
71
|
'action.wrapOff': 'Disable word wrap (long lines scroll horizontally)',
|
|
72
|
+
'action.mdRender': 'Rendered preview',
|
|
73
|
+
'action.mdSource': 'Show markdown source',
|
|
36
74
|
'tab.close': 'Close tab',
|
|
37
75
|
'tab.collapse': 'Collapse file details',
|
|
38
76
|
'tab.expand': 'Expand file details',
|
|
77
|
+
'tab.diskChanged': 'File changed on disk — click to reload (discards unsaved edits)',
|
|
78
|
+
'menu.open': 'Open preview',
|
|
79
|
+
'menu.newFile': 'New File',
|
|
80
|
+
'menu.newFolder': 'New Folder',
|
|
81
|
+
'menu.rename': 'Rename',
|
|
82
|
+
'menu.delete': 'Delete',
|
|
83
|
+
'menu.copyPath': 'Copy Path',
|
|
84
|
+
'menu.copy': 'Copy',
|
|
85
|
+
'menu.cut': 'Cut',
|
|
86
|
+
'menu.paste': 'Paste',
|
|
87
|
+
'menu.refresh': 'Refresh',
|
|
88
|
+
'menu.openSystem': 'Open in System',
|
|
89
|
+
'menu.revealInExplorer': 'Reveal in Explorer',
|
|
90
|
+
'menu.deleteSelected': 'Delete {count} selected item(s)',
|
|
91
|
+
'action.undo': 'Undo (Ctrl+Z)',
|
|
92
|
+
'undo.copy': 'Copy "{name}"',
|
|
93
|
+
'undo.move': 'Move "{name}"',
|
|
94
|
+
'undo.rename': 'Rename "{name}"',
|
|
95
|
+
'undo.create': 'Create "{name}"',
|
|
96
|
+
'undo.delete': 'Delete "{name}"',
|
|
97
|
+
'error.reveal': 'Failed to reveal in Explorer',
|
|
98
|
+
'prompt.newFileName': 'New file name',
|
|
99
|
+
'prompt.newFolderName': 'New folder name',
|
|
100
|
+
'prompt.renameTo': 'Rename to',
|
|
101
|
+
'confirm.deleteFile': 'Delete file "{name}"? (undoable)',
|
|
102
|
+
'confirm.deleteDir': 'Delete folder "{name}" and all its contents? (undoable)',
|
|
103
|
+
'confirm.deleteSelected': 'Delete {count} selected item(s)? {names} (undoable)',
|
|
104
|
+
'confirm.overwrite': '"{name}" already exists. Overwrite?',
|
|
105
|
+
'clipboard.copied': '{count} item(s) copied',
|
|
106
|
+
'clipboard.cut': '{count} item(s) cut',
|
|
107
|
+
'clipboard.pasteHint': ' · Ctrl+V to paste here',
|
|
108
|
+
'clipboard.clear': 'Clear clipboard',
|
|
39
109
|
'preview.loading': 'Loading…',
|
|
40
110
|
'preview.binary': 'Binary file, cannot preview',
|
|
111
|
+
'preview.imageFailed': 'Failed to load image',
|
|
41
112
|
'preview.tooLarge': 'File too large to preview (limit {limit})',
|
|
42
113
|
'preview.emptyDir': 'Empty directory',
|
|
43
114
|
'preview.error': 'Load failed',
|
|
44
115
|
'preview.highlightOff': 'Large file: syntax highlighting off (still editable)',
|
|
116
|
+
'preview.mdRenderOff': 'Large file: source view (still editable)',
|
|
45
117
|
}
|