Skip to content

Commit cfc2fc7

Browse files
committed
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
1 parent 0ec30e2 commit cfc2fc7

File tree

2 files changed

+78
-70
lines changed

2 files changed

+78
-70
lines changed

app/components/workbench/DiffView.tsx

Lines changed: 67 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -542,8 +542,53 @@ const FileInfo = memo(
542542
},
543543
);
544544

545+
// Create and manage a single highlighter instance at the module level
546+
let highlighterInstance: any = null;
547+
let highlighterPromise: Promise<any> | null = null;
548+
549+
const getSharedHighlighter = async () => {
550+
if (highlighterInstance) {
551+
return highlighterInstance;
552+
}
553+
554+
if (highlighterPromise) {
555+
return highlighterPromise;
556+
}
557+
558+
highlighterPromise = getHighlighter({
559+
themes: ['github-dark', 'github-light'],
560+
langs: [
561+
'typescript',
562+
'javascript',
563+
'json',
564+
'html',
565+
'css',
566+
'jsx',
567+
'tsx',
568+
'python',
569+
'php',
570+
'java',
571+
'c',
572+
'cpp',
573+
'csharp',
574+
'go',
575+
'ruby',
576+
'rust',
577+
'plaintext',
578+
],
579+
});
580+
581+
highlighterInstance = await highlighterPromise;
582+
highlighterPromise = null;
583+
584+
// Clear the promise once resolved
585+
return highlighterInstance;
586+
};
587+
545588
const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language }: CodeComparisonProps) => {
546589
const [isFullscreen, setIsFullscreen] = useState(false);
590+
591+
// Use state to hold the shared highlighter instance
547592
const [highlighter, setHighlighter] = useState<any>(null);
548593
const theme = useStore(themeStore);
549594

@@ -554,34 +599,32 @@ const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language }
554599
const { unifiedBlocks, hasChanges, isBinary, error } = useProcessChanges(beforeCode, afterCode);
555600

556601
useEffect(() => {
557-
getHighlighter({
558-
themes: ['github-dark', 'github-light'],
559-
langs: [
560-
'typescript',
561-
'javascript',
562-
'json',
563-
'html',
564-
'css',
565-
'jsx',
566-
'tsx',
567-
'python',
568-
'php',
569-
'java',
570-
'c',
571-
'cpp',
572-
'csharp',
573-
'go',
574-
'ruby',
575-
'rust',
576-
'plaintext',
577-
],
578-
}).then(setHighlighter);
579-
}, []);
602+
// Fetch the shared highlighter instance
603+
getSharedHighlighter().then(setHighlighter);
604+
605+
/*
606+
* No cleanup needed here for the highlighter instance itself,
607+
* as it's managed globally. Shiki instances don't typically
608+
* need disposal unless you are dynamically loading/unloading themes/languages.
609+
* If you were dynamically loading, you might need a more complex
610+
* shared instance manager with reference counting or similar.
611+
* For static themes/langs, a single instance is sufficient.
612+
*/
613+
}, []); // Empty dependency array ensures this runs only once on mount
580614

581615
if (isBinary || error) {
582616
return renderContentWarning(isBinary ? 'binary' : 'error');
583617
}
584618

619+
// Render a loading state or null while highlighter is not ready
620+
if (!highlighter) {
621+
return (
622+
<div className="h-full flex items-center justify-center">
623+
<div className="text-bolt-elements-textTertiary">Loading diff...</div>
624+
</div>
625+
);
626+
}
627+
585628
return (
586629
<FullscreenOverlay isFullscreen={isFullscreen}>
587630
<div className="w-full h-full flex flex-col">
@@ -602,7 +645,7 @@ const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language }
602645
lineNumber={block.lineNumber}
603646
content={block.content}
604647
type={block.type}
605-
highlighter={highlighter}
648+
highlighter={highlighter} // Pass the shared instance
606649
language={language}
607650
block={block}
608651
theme={theme}

app/lib/stores/files.ts

Lines changed: 11 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -692,27 +692,9 @@ export class FilesStore {
692692
#processEventBuffer(events: Array<[events: PathWatcherEvent[]]>) {
693693
const watchEvents = events.flat(2);
694694

695-
for (const { type, path: eventPath, buffer } of watchEvents) {
695+
for (const { type, path, buffer } of watchEvents) {
696696
// remove any trailing slashes
697-
const sanitizedPath = eventPath.replace(/\/+$/g, '');
698-
699-
// Skip processing if this file/folder was explicitly deleted
700-
if (this.#deletedPaths.has(sanitizedPath)) {
701-
continue;
702-
}
703-
704-
let isInDeletedFolder = false;
705-
706-
for (const deletedPath of this.#deletedPaths) {
707-
if (sanitizedPath.startsWith(deletedPath + '/')) {
708-
isInDeletedFolder = true;
709-
break;
710-
}
711-
}
712-
713-
if (isInDeletedFolder) {
714-
continue;
715-
}
697+
const sanitizedPath = path.replace(/\/+$/g, '');
716698

717699
switch (type) {
718700
case 'add_dir': {
@@ -738,38 +720,21 @@ export class FilesStore {
738720
}
739721

740722
let content = '';
723+
724+
/**
725+
* @note This check is purely for the editor. The way we detect this is not
726+
* bullet-proof and it's a best guess so there might be false-positives.
727+
* The reason we do this is because we don't want to display binary files
728+
* in the editor nor allow to edit them.
729+
*/
741730
const isBinary = isBinaryFile(buffer);
742731

743-
if (isBinary && buffer) {
744-
// For binary files, we need to preserve the content as base64
745-
content = Buffer.from(buffer).toString('base64');
746-
} else if (!isBinary) {
732+
if (!isBinary) {
747733
content = this.#decodeFileContent(buffer);
748-
749-
/*
750-
* If the content is a single space and this is from our empty file workaround,
751-
* convert it back to an actual empty string
752-
*/
753-
if (content === ' ' && type === 'add_file') {
754-
content = '';
755-
}
756-
}
757-
758-
const existingFile = this.files.get()[sanitizedPath];
759-
760-
if (existingFile?.type === 'file' && existingFile.isBinary && existingFile.content && !content) {
761-
content = existingFile.content;
762734
}
763735

764-
// Preserve lock state if the file already exists
765-
const isLocked = existingFile?.type === 'file' ? existingFile.isLocked : false;
736+
this.files.setKey(sanitizedPath, { type: 'file', content, isBinary });
766737

767-
this.files.setKey(sanitizedPath, {
768-
type: 'file',
769-
content,
770-
isBinary,
771-
isLocked,
772-
});
773738
break;
774739
}
775740
case 'remove_file': {

0 commit comments

Comments
 (0)