Skip to content

Commit 1d1c9f7

Browse files
committed
refactor: Phase 1 - Performance & code quality optimizations
- Extract search logic into dedicated hooks/useSearch.ts - Remove unused isOnline state and fuzzySearch option - Replace FileTree switch statement with Map for O(1) icon lookups - Add proper hljs type declarations (types/hljs.d.ts) - Move inline CSS from index.html to index.css - Improve search dialog positioning for small viewports - Add corrupted localStorage cleanup and quota error handling - Extract buildSearchRegex helper in CodeView for reuse - Fix marked.setOptions configuration (removed from useAppLogic) - Code formatting and dependency array fixes
1 parent 7ef714b commit 1d1c9f7

10 files changed

Lines changed: 563 additions & 385 deletions

File tree

structure-insight/components/CodeView.tsx

Lines changed: 88 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import { FileContent, SearchOptions } from '../types';
55
import { marked } from 'marked';
66
import DOMPurify from 'dompurify';
77

8-
// hljs is loaded globally via index.html script tag
9-
declare const hljs: any;
8+
// hljs types provided by types/hljs.d.ts
109

1110
interface FileCardProps {
1211
file: FileContent;
@@ -32,91 +31,105 @@ const IconButton: React.FC<{icon: string, title: string, onClick: () => void, di
3231
</button>
3332
);
3433

34+
/** Build a regex from search options, returning null if invalid */
35+
function buildSearchRegex(query: string, options: SearchOptions): RegExp | null {
36+
if (!query.trim()) return null;
37+
const flags = options.caseSensitive ? 'g' : 'gi';
38+
let pattern = options.useRegex ? query : query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
39+
if (options.wholeWord && !options.useRegex) {
40+
pattern = `\\b${pattern}\\b`;
41+
}
42+
try {
43+
return new RegExp(pattern, flags);
44+
} catch {
45+
return null;
46+
}
47+
}
3548

36-
const FileCard: React.FC<FileCardProps> = ({
37-
file, isEditing, onStartEdit, onSaveEdit, onCancelEdit,
49+
const FileCard: React.FC<FileCardProps> = ({
50+
file, isEditing, onStartEdit, onSaveEdit, onCancelEdit,
3851
isMarkdown, isMarkdownPreview, onToggleMarkdownPreview, onShowToast, fontSize,
3952
searchQuery, searchOptions, activeMatchIndexInFile, onCopyPath
4053
}) => {
4154
const [editText, setEditText] = React.useState(file.content);
4255
const codeRef = React.useRef<HTMLElement>(null);
43-
56+
const highlightTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
57+
4458
React.useEffect(() => {
4559
setEditText(file.content);
4660
}, [file.content]);
4761

48-
// Handle syntax highlighting and search highlighting
62+
// Cleanup highlight timer on unmount
63+
React.useEffect(() => {
64+
return () => {
65+
if (highlightTimerRef.current) clearTimeout(highlightTimerRef.current);
66+
};
67+
}, []);
68+
69+
// Handle syntax highlighting and debounced search highlighting
4970
React.useEffect(() => {
50-
if (codeRef.current && !isEditing && !isMarkdownPreview && !file.excluded) {
51-
// 1. Set content and syntax highlight
52-
codeRef.current.textContent = file.content;
53-
hljs.highlightElement(codeRef.current);
54-
55-
// 2. Apply search highlighting if query exists
56-
if (searchQuery.trim()) {
57-
try {
58-
const flags = searchOptions.caseSensitive ? 'g' : 'gi';
59-
let pattern = searchOptions.useRegex ? searchQuery : searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
60-
if (searchOptions.wholeWord && !searchOptions.useRegex) {
61-
pattern = `\\b${pattern}\\b`;
62-
}
63-
const regex = new RegExp(pattern, flags);
64-
65-
// Use TreeWalker to find text nodes to replace with highlights
66-
const walker = document.createTreeWalker(codeRef.current, NodeFilter.SHOW_TEXT, null);
67-
const textNodes: Text[] = [];
68-
let node: Node | null;
69-
while ((node = walker.nextNode())) {
70-
textNodes.push(node as Text);
71-
}
72-
73-
let globalMatchIndex = 0;
74-
75-
textNodes.forEach(textNode => {
76-
const text = textNode.nodeValue;
77-
if (!text) return;
78-
79-
const matches = [...text.matchAll(regex)];
80-
if (matches.length === 0) return;
81-
82-
const fragment = document.createDocumentFragment();
83-
let lastIndex = 0;
84-
85-
matches.forEach(match => {
86-
// Text before match
87-
if (match.index! > lastIndex) {
88-
fragment.appendChild(document.createTextNode(text.substring(lastIndex, match.index!)));
89-
}
90-
91-
// The Match
92-
const mark = document.createElement('mark');
93-
mark.className = 'search-highlight'; // Base class
94-
// Apply active class if this is the currently selected match
95-
if (globalMatchIndex === activeMatchIndexInFile) {
96-
mark.classList.add('search-highlight-active');
97-
// Scroll active match into view
98-
setTimeout(() => mark.scrollIntoView({ behavior: 'smooth', block: 'center' }), 0);
99-
}
100-
mark.textContent = match[0];
101-
fragment.appendChild(mark);
102-
103-
globalMatchIndex++;
104-
lastIndex = match.index! + match[0].length;
105-
});
106-
107-
// Text after last match
108-
if (lastIndex < text.length) {
109-
fragment.appendChild(document.createTextNode(text.substring(lastIndex)));
110-
}
111-
112-
textNode.parentNode?.replaceChild(fragment, textNode);
113-
});
114-
115-
} catch (e) {
116-
console.debug("Search highlight error (regex likely invalid yet):", e);
117-
}
71+
if (!codeRef.current || isEditing || isMarkdownPreview || file.excluded) return;
72+
73+
// 1. Set content and syntax highlight
74+
codeRef.current.textContent = file.content;
75+
hljs.highlightElement(codeRef.current);
76+
77+
// 2. Debounce search highlighting
78+
if (highlightTimerRef.current) clearTimeout(highlightTimerRef.current);
79+
80+
if (!searchQuery.trim()) return;
81+
82+
const regex = buildSearchRegex(searchQuery, searchOptions);
83+
if (!regex) return;
84+
85+
highlightTimerRef.current = setTimeout(() => {
86+
if (!codeRef.current) return;
87+
88+
// Use TreeWalker to find text nodes to replace with highlights
89+
const walker = document.createTreeWalker(codeRef.current, NodeFilter.SHOW_TEXT, null);
90+
const textNodes: Text[] = [];
91+
let node: Node | null;
92+
while ((node = walker.nextNode())) {
93+
textNodes.push(node as Text);
11894
}
119-
}
95+
96+
let globalMatchIndex = 0;
97+
98+
textNodes.forEach(textNode => {
99+
const text = textNode.nodeValue;
100+
if (!text) return;
101+
102+
const matches = [...text.matchAll(regex)];
103+
if (matches.length === 0) return;
104+
105+
const fragment = document.createDocumentFragment();
106+
let lastIndex = 0;
107+
108+
matches.forEach(match => {
109+
if (match.index! > lastIndex) {
110+
fragment.appendChild(document.createTextNode(text.substring(lastIndex, match.index!)));
111+
}
112+
113+
const mark = document.createElement('mark');
114+
mark.className = 'search-highlight';
115+
if (globalMatchIndex === activeMatchIndexInFile) {
116+
mark.classList.add('search-highlight-active');
117+
setTimeout(() => mark.scrollIntoView({ behavior: 'smooth', block: 'center' }), 0);
118+
}
119+
mark.textContent = match[0];
120+
fragment.appendChild(mark);
121+
122+
globalMatchIndex++;
123+
lastIndex = match.index! + match[0].length;
124+
});
125+
126+
if (lastIndex < text.length) {
127+
fragment.appendChild(document.createTextNode(text.substring(lastIndex)));
128+
}
129+
130+
textNode.parentNode?.replaceChild(fragment, textNode);
131+
});
132+
}, 150);
120133
}, [file, isEditing, isMarkdownPreview, searchQuery, searchOptions, activeMatchIndexInFile]);
121134

122135

0 commit comments

Comments
 (0)