refactor(files): simplify file event processing logic

Remove redundant checks for deleted paths and streamline binary file handling. This fixes the browser using excessive memory and freezing.

Improve DiffView to use a singleton instance of Shiki
This commit is contained in:
KevIsDev 2025-05-20 00:57:52 +01:00
parent 0ec30e2358
commit cfc2fc75d8
2 changed files with 78 additions and 70 deletions

View File

@ -542,8 +542,53 @@ const FileInfo = memo(
}, },
); );
// Create and manage a single highlighter instance at the module level
let highlighterInstance: any = null;
let highlighterPromise: Promise<any> | null = null;
const getSharedHighlighter = async () => {
if (highlighterInstance) {
return highlighterInstance;
}
if (highlighterPromise) {
return highlighterPromise;
}
highlighterPromise = getHighlighter({
themes: ['github-dark', 'github-light'],
langs: [
'typescript',
'javascript',
'json',
'html',
'css',
'jsx',
'tsx',
'python',
'php',
'java',
'c',
'cpp',
'csharp',
'go',
'ruby',
'rust',
'plaintext',
],
});
highlighterInstance = await highlighterPromise;
highlighterPromise = null;
// Clear the promise once resolved
return highlighterInstance;
};
const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language }: CodeComparisonProps) => { const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language }: CodeComparisonProps) => {
const [isFullscreen, setIsFullscreen] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false);
// Use state to hold the shared highlighter instance
const [highlighter, setHighlighter] = useState<any>(null); const [highlighter, setHighlighter] = useState<any>(null);
const theme = useStore(themeStore); const theme = useStore(themeStore);
@ -554,34 +599,32 @@ const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language }
const { unifiedBlocks, hasChanges, isBinary, error } = useProcessChanges(beforeCode, afterCode); const { unifiedBlocks, hasChanges, isBinary, error } = useProcessChanges(beforeCode, afterCode);
useEffect(() => { useEffect(() => {
getHighlighter({ // Fetch the shared highlighter instance
themes: ['github-dark', 'github-light'], getSharedHighlighter().then(setHighlighter);
langs: [
'typescript', /*
'javascript', * No cleanup needed here for the highlighter instance itself,
'json', * as it's managed globally. Shiki instances don't typically
'html', * need disposal unless you are dynamically loading/unloading themes/languages.
'css', * If you were dynamically loading, you might need a more complex
'jsx', * shared instance manager with reference counting or similar.
'tsx', * For static themes/langs, a single instance is sufficient.
'python', */
'php', }, []); // Empty dependency array ensures this runs only once on mount
'java',
'c',
'cpp',
'csharp',
'go',
'ruby',
'rust',
'plaintext',
],
}).then(setHighlighter);
}, []);
if (isBinary || error) { if (isBinary || error) {
return renderContentWarning(isBinary ? 'binary' : 'error'); return renderContentWarning(isBinary ? 'binary' : 'error');
} }
// Render a loading state or null while highlighter is not ready
if (!highlighter) {
return (
<div className="h-full flex items-center justify-center">
<div className="text-bolt-elements-textTertiary">Loading diff...</div>
</div>
);
}
return ( return (
<FullscreenOverlay isFullscreen={isFullscreen}> <FullscreenOverlay isFullscreen={isFullscreen}>
<div className="w-full h-full flex flex-col"> <div className="w-full h-full flex flex-col">
@ -602,7 +645,7 @@ const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language }
lineNumber={block.lineNumber} lineNumber={block.lineNumber}
content={block.content} content={block.content}
type={block.type} type={block.type}
highlighter={highlighter} highlighter={highlighter} // Pass the shared instance
language={language} language={language}
block={block} block={block}
theme={theme} theme={theme}

View File

@ -692,27 +692,9 @@ export class FilesStore {
#processEventBuffer(events: Array<[events: PathWatcherEvent[]]>) { #processEventBuffer(events: Array<[events: PathWatcherEvent[]]>) {
const watchEvents = events.flat(2); const watchEvents = events.flat(2);
for (const { type, path: eventPath, buffer } of watchEvents) { for (const { type, path, buffer } of watchEvents) {
// remove any trailing slashes // remove any trailing slashes
const sanitizedPath = eventPath.replace(/\/+$/g, ''); const sanitizedPath = path.replace(/\/+$/g, '');
// Skip processing if this file/folder was explicitly deleted
if (this.#deletedPaths.has(sanitizedPath)) {
continue;
}
let isInDeletedFolder = false;
for (const deletedPath of this.#deletedPaths) {
if (sanitizedPath.startsWith(deletedPath + '/')) {
isInDeletedFolder = true;
break;
}
}
if (isInDeletedFolder) {
continue;
}
switch (type) { switch (type) {
case 'add_dir': { case 'add_dir': {
@ -738,38 +720,21 @@ export class FilesStore {
} }
let content = ''; let content = '';
/**
* @note This check is purely for the editor. The way we detect this is not
* bullet-proof and it's a best guess so there might be false-positives.
* The reason we do this is because we don't want to display binary files
* in the editor nor allow to edit them.
*/
const isBinary = isBinaryFile(buffer); const isBinary = isBinaryFile(buffer);
if (isBinary && buffer) { if (!isBinary) {
// For binary files, we need to preserve the content as base64
content = Buffer.from(buffer).toString('base64');
} else if (!isBinary) {
content = this.#decodeFileContent(buffer); content = this.#decodeFileContent(buffer);
/*
* If the content is a single space and this is from our empty file workaround,
* convert it back to an actual empty string
*/
if (content === ' ' && type === 'add_file') {
content = '';
}
} }
const existingFile = this.files.get()[sanitizedPath]; this.files.setKey(sanitizedPath, { type: 'file', content, isBinary });
if (existingFile?.type === 'file' && existingFile.isBinary && existingFile.content && !content) {
content = existingFile.content;
}
// Preserve lock state if the file already exists
const isLocked = existingFile?.type === 'file' ? existingFile.isLocked : false;
this.files.setKey(sanitizedPath, {
type: 'file',
content,
isBinary,
isLocked,
});
break; break;
} }
case 'remove_file': { case 'remove_file': {