/ concept-collection / minwebide-demo
Sign in
concept-collection / minwebide-demo
minwebide-demo / src / customEditors.ts
144 lines · 4.8 KBBlameHistoryRaw
1import { Delayer } from 'vs/base/common/async';
2import { DisposableStore } from 'vs/base/common/lifecycle';
3import type { CustomEditorPane, CustomEditorProvider } from 'minwebide';
4import './customEditors.css';
6// Three example custom editors exercising the app-level custom editor API.
7// The registration shape mirrors VS Code's `contributes.customEditors`.
9/**
10 * Markdown preview (priority 'option': the text editor stays the default,
11 * a tab-bar action offers the preview). Rendered by VS Code's own
12 * vs/base markdownRenderer, live-updating against the shared text model.
13 */
14export const markdownPreview: CustomEditorProvider = {
15 viewType: 'demo.markdownPreview',
16 displayName: 'Markdown Preview',
17 selector: [{ filenamePattern: '*.md' }],
18 priority: 'option',
19 async resolveCustomEditor(document): Promise<CustomEditorPane> {
20 // markdownRenderer drags in marked + dompurify; load it only when a
21 // preview is actually opened
22 const { renderMarkdown } = await import('vs/base/browser/markdownRenderer');
23 const disposables = new DisposableStore();
24 const element = window.document.createElement('div');
25 element.className = 'demo-markdown-preview';
27 const model = await document.getTextModel();
28 const render = () => {
29 const rendered = disposables.add(renderMarkdown({ value: model.getValue() }));
30 element.replaceChildren(rendered.element);
31 };
32 const delayer = disposables.add(new Delayer<void>(200));
33 disposables.add(model.onDidChangeContent(() => delayer.trigger(() => render())));
34 render();
36 return { element, dispose: () => disposables.dispose() };
37 },
38};
40function parseCsvLine(line: string): string[] {
41 const cells: string[] = [];
42 let current = '';
43 let inQuotes = false;
44 for (let i = 0; i < line.length; i++) {
45 const ch = line[i];
46 if (inQuotes) {
47 if (ch === '"' && line[i + 1] === '"') {
48 current += '"';
49 i++;
50 } else if (ch === '"') {
51 inQuotes = false;
52 } else {
53 current += ch;
54 }
55 } else if (ch === '"') {
56 inQuotes = true;
57 } else if (ch === ',') {
58 cells.push(current);
59 current = '';
60 } else {
61 current += ch;
62 }
63 }
64 cells.push(current);
65 return cells;
68/**
69 * CSV table viewer (priority 'default': replaces the text editor for .csv;
70 * "Reopen as Text Editor" stays available in the tab bar).
71 */
72export const csvViewer: CustomEditorProvider = {
73 viewType: 'demo.csvViewer',
74 displayName: 'CSV Table',
75 selector: [{ filenamePattern: '*.csv' }],
76 priority: 'default',
77 async resolveCustomEditor(document): Promise<CustomEditorPane> {
78 const disposables = new DisposableStore();
79 const element = window.document.createElement('div');
80 element.className = 'demo-csv-viewer';
82 const model = await document.getTextModel();
83 const render = () => {
84 const lines = model.getValue().split(/\r\n|\r|\n/).filter(line => line.length > 0);
85 const table = window.document.createElement('table');
86 lines.forEach((line, index) => {
87 const row = table.insertRow();
88 for (const cell of parseCsvLine(line)) {
89 const cellEl = window.document.createElement(index === 0 ? 'th' : 'td');
90 cellEl.textContent = cell;
91 row.appendChild(cellEl);
92 }
93 });
94 element.replaceChildren(table);
95 };
96 const delayer = disposables.add(new Delayer<void>(200));
97 disposables.add(model.onDidChangeContent(() => delayer.trigger(() => render())));
98 render();
100 return { element, dispose: () => disposables.dispose() };
101 },
102};
104const IMAGE_MIME: Record<string, string> = {
105 png: 'image/png',
106 jpg: 'image/jpeg',
107 jpeg: 'image/jpeg',
108 gif: 'image/gif',
109 webp: 'image/webp',
110 svg: 'image/svg+xml',
111};
113/**
114 * Image viewer — exercises the binary document path (readBytes).
115 */
116export const imageViewer: CustomEditorProvider = {
117 viewType: 'demo.imageViewer',
118 displayName: 'Image Viewer',
119 selector: [{ filenamePattern: '*.{png,jpg,jpeg,gif,webp,svg}' }],
120 priority: 'default',
121 async resolveCustomEditor(document): Promise<CustomEditorPane> {
122 const element = window.document.createElement('div');
123 element.className = 'demo-image-viewer';
125 const bytes = await document.readBytes();
126 const extension = document.uri.path.split('.').pop()?.toLowerCase() ?? '';
127 const blob = new Blob([bytes as BlobPart], { type: IMAGE_MIME[extension] ?? 'application/octet-stream' });
128 const url = URL.createObjectURL(blob);
130 const img = window.document.createElement('img');
131 img.src = url;
132 element.appendChild(img);
133 const caption = window.document.createElement('div');
134 caption.className = 'demo-image-caption';
135 img.addEventListener('load', () => {
136 caption.textContent = `${img.naturalWidth} × ${img.naturalHeight} ${bytes.byteLength.toLocaleString()} bytes`;
137 });
138 element.appendChild(caption);
140 return { element, dispose: () => URL.revokeObjectURL(url) };
141 },
142};
144export const demoCustomEditors: CustomEditorProvider[] = [markdownPreview, csvViewer, imageViewer];
moveopenescclose