spacedrive/interface/components/TextViewer/index.tsx
Oscar Beaumont 949707c7f9
Vite upgrades (#1911)
* Upgrade Vite + use SWC React plugin

* Upgrade to type module

* lazy load Sentry

* Lazy load prism

* fix

* Lazy load some of the icons

* fix types

* Fix eslint config

* run lint --fix

* Upgrade Turbo

* Turborepo not happy

* Upgrade Typescript

* Stop complaining Turborepo
2024-01-02 06:26:28 +00:00

156 lines
3.9 KiB
TypeScript

import { useVirtualizer, VirtualItem } from '@tanstack/react-virtual';
import clsx from 'clsx';
import { memo, useEffect, useRef, useState } from 'react';
import { languageMapping } from './prism';
const prismaLazy = import('./prism-lazy');
export interface TextViewerProps {
src: string;
className?: string;
onLoad?: (event: HTMLElementEventMap['load']) => void;
onError?: (event: HTMLElementEventMap['error']) => void;
codeExtension?: string;
isSidebarPreview?: boolean;
}
// TODO: ANSI support
export const TextViewer = memo(
({ src, className, onLoad, onError, codeExtension, isSidebarPreview }: TextViewerProps) => {
const [lines, setLines] = useState<string[]>([]);
const parentRef = useRef<HTMLPreElement>(null);
const rowVirtualizer = useVirtualizer({
count: lines.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 22
});
useEffect(() => {
// Ignore empty urls
if (!src || src === '#') return;
const controller = new AbortController();
fetch(src, {
mode: 'cors',
signal: controller.signal
})
.then((response) => {
if (!response.ok) throw new Error(`Invalid response: ${response.statusText}`);
if (!response.body) return;
onLoad?.(new UIEvent('load', {}));
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
return reader.read().then(function ingestLines({
done,
value
}): void | Promise<void> {
if (done) return;
const chunks = value.split('\n');
setLines((lines) => [...lines, ...chunks]);
if (isSidebarPreview) return;
// Read some more, and call this function again
return reader.read().then(ingestLines);
});
})
.catch((error) => {
if (!controller.signal.aborted)
onError?.(new ErrorEvent('error', { message: `${error}` }));
});
return () => controller.abort();
}, [src, onError, onLoad, codeExtension, isSidebarPreview]);
return (
<pre ref={parentRef} tabIndex={0} className={className}>
<div
tabIndex={0}
className={clsx(
'relative w-full whitespace-pre text-sm text-ink',
codeExtension &&
`language-${languageMapping.get(codeExtension) ?? codeExtension}`
)}
style={{
height: `${rowVirtualizer.getTotalSize()}px`
}}
>
{rowVirtualizer.getVirtualItems().map((row) => (
<TextRow
key={row.key}
codeExtension={codeExtension}
row={row}
content={lines[row.index]!}
/>
))}
</div>
</pre>
);
}
);
function TextRow({
codeExtension,
row,
content
}: {
codeExtension?: string;
row: VirtualItem;
content: string;
}) {
const contentRef = useRef<HTMLSpanElement>(null);
useEffect(() => {
if (contentRef.current) {
const cb: IntersectionObserverCallback = async (events) => {
for (const event of events) {
if (
!event.isIntersecting ||
contentRef.current?.getAttribute('data-highlighted') === 'true'
)
continue;
contentRef.current?.setAttribute('data-highlighted', 'true');
(await prismaLazy).highlightElement(event.target, false); // Prism's async seems to be broken
// With this class present TOML headers are broken Eg. `[dependencies]` will format over multiple lines
const children = contentRef.current?.children;
if (children) {
for (const elem of children) {
elem.classList.remove('table');
}
}
}
};
new IntersectionObserver(cb).observe(contentRef.current);
}
}, []);
return (
<div
className={clsx('absolute left-0 top-0 flex w-full whitespace-pre')}
style={{
height: `${row.size}px`,
transform: `translateY(${row.start}px)`
}}
>
{codeExtension && (
<div
key={row.key}
className={clsx(
'token block shrink-0 whitespace-pre pl-2 pr-4 text-sm leading-6 text-gray-450'
)}
>
{row.index + 1}
</div>
)}
<span ref={contentRef} className="flex-1 pl-2">
{content}
</span>
</div>
);
}