mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
## CHANGES
- Search result scrolling now centers correctly within scrollable preview container 🎯 - Search scrolling clamps to non-negative positions for safer navigation 🛡️ - Edit-mode search no longer steals focus while you type—huge UX win ✍️ - Edit-mode search now resets match counts cleanly when query clears 🧹 - Match selection triggers only during navigation (Enter/Shift+Enter) ⌨️ - Added tracking refs to detect match navigation versus query edits 🧠 - Git status widget is now memoized to avoid needless re-renders 🚀 - Session hover tooltip content is memoized for smoother session lists 🧊
This commit is contained in:
@@ -960,13 +960,17 @@ export const FilePreview = forwardRef<FilePreviewHandle, FilePreviewProps>(funct
|
||||
// Scroll to current match
|
||||
const currentRange = allRanges[targetIndex];
|
||||
const rect = currentRange.getBoundingClientRect();
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const scrollParent = contentRef.current;
|
||||
|
||||
if (scrollParent && rect) {
|
||||
// Calculate scroll position to center the match
|
||||
const scrollTop = scrollParent.scrollTop + rect.top - containerRect.top - scrollParent.clientHeight / 2 + rect.height / 2;
|
||||
scrollParent.scrollTo({ top: scrollTop, behavior: 'smooth' });
|
||||
// Calculate position of the match relative to the scroll container's top
|
||||
// rect.top is viewport-relative, so we need to account for current scroll
|
||||
// and the scroll container's viewport position
|
||||
const scrollContainerRect = scrollParent.getBoundingClientRect();
|
||||
const matchOffsetInScrollContainer = rect.top - scrollContainerRect.top + scrollParent.scrollTop;
|
||||
// Calculate scroll position to center the match vertically
|
||||
const scrollTop = matchOffsetInScrollContainer - scrollParent.clientHeight / 2 + rect.height / 2;
|
||||
scrollParent.scrollTo({ top: Math.max(0, scrollTop), behavior: 'smooth' });
|
||||
}
|
||||
} else {
|
||||
(CSS as any).highlights.delete('search-results');
|
||||
@@ -1104,9 +1108,18 @@ export const FilePreview = forwardRef<FilePreviewHandle, FilePreviewProps>(funct
|
||||
return formatShortcutKeys(shortcut.keys);
|
||||
};
|
||||
|
||||
// Handle search in edit mode - jump to and select the match in textarea
|
||||
// Track previous search query and match index for edit mode navigation
|
||||
const prevSearchQueryRef = useRef<string>('');
|
||||
const prevMatchIndexRef = useRef<number>(0);
|
||||
|
||||
// Handle search in edit mode - count matches and update state
|
||||
// Note: We separate counting from selection to avoid stealing focus while typing
|
||||
useEffect(() => {
|
||||
if (!isEditableText || !markdownEditMode || !searchQuery.trim() || !textareaRef.current) {
|
||||
if (isEditableText && markdownEditMode) {
|
||||
setTotalMatches(0);
|
||||
setCurrentMatchIndex(0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1134,20 +1147,29 @@ export const FilePreview = forwardRef<FilePreviewHandle, FilePreviewProps>(funct
|
||||
return;
|
||||
}
|
||||
|
||||
// Select the current match in the textarea
|
||||
const currentMatch = matches[validIndex];
|
||||
if (currentMatch) {
|
||||
const textarea = textareaRef.current;
|
||||
textarea.focus();
|
||||
textarea.setSelectionRange(currentMatch.start, currentMatch.end);
|
||||
// Only scroll and select when navigating between matches (Enter/Shift+Enter)
|
||||
// or when search query is complete (user stopped typing)
|
||||
// We detect navigation by checking if currentMatchIndex changed without searchQuery changing
|
||||
const isNavigating = prevSearchQueryRef.current === searchQuery && prevMatchIndexRef.current !== currentMatchIndex;
|
||||
prevSearchQueryRef.current = searchQuery;
|
||||
prevMatchIndexRef.current = currentMatchIndex;
|
||||
|
||||
// Scroll to make the selection visible
|
||||
// Calculate approximate line number and scroll to it
|
||||
const textBeforeMatch = content.substring(0, currentMatch.start);
|
||||
const lineNumber = textBeforeMatch.split('\n').length;
|
||||
const lineHeight = parseInt(getComputedStyle(textarea).lineHeight) || 24;
|
||||
const targetScroll = (lineNumber - 5) * lineHeight; // Leave some lines above
|
||||
textarea.scrollTop = Math.max(0, targetScroll);
|
||||
// Select the current match in the textarea only when navigating
|
||||
if (isNavigating) {
|
||||
const currentMatch = matches[validIndex];
|
||||
if (currentMatch) {
|
||||
const textarea = textareaRef.current;
|
||||
textarea.focus();
|
||||
textarea.setSelectionRange(currentMatch.start, currentMatch.end);
|
||||
|
||||
// Scroll to make the selection visible
|
||||
// Calculate approximate line number and scroll to it
|
||||
const textBeforeMatch = content.substring(0, currentMatch.start);
|
||||
const lineNumber = textBeforeMatch.split('\n').length;
|
||||
const lineHeight = parseInt(getComputedStyle(textarea).lineHeight) || 24;
|
||||
const targetScroll = (lineNumber - 5) * lineHeight; // Leave some lines above
|
||||
textarea.scrollTop = Math.max(0, targetScroll);
|
||||
}
|
||||
}
|
||||
}, [searchQuery, currentMatchIndex, isEditableText, markdownEditMode, editContent]);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useState, useRef, useEffect, memo } from 'react';
|
||||
import { GitBranch, Plus, Minus, FileEdit, FileDiff } from 'lucide-react';
|
||||
import type { Theme } from '../types';
|
||||
import { useGitStatus, type GitFileChange } from '../contexts/GitStatusContext';
|
||||
@@ -22,8 +22,10 @@ interface GitStatusWidgetProps {
|
||||
*
|
||||
* The context provides detailed file changes (with line additions/deletions)
|
||||
* only for the active session. Non-active sessions will show basic file counts.
|
||||
*
|
||||
* PERF: Memoized to prevent re-renders when parent re-renders with same props.
|
||||
*/
|
||||
export function GitStatusWidget({ sessionId, isGitRepo, theme, onViewDiff, compact = false }: GitStatusWidgetProps) {
|
||||
export const GitStatusWidget = memo(function GitStatusWidget({ sessionId, isGitRepo, theme, onViewDiff, compact = false }: GitStatusWidgetProps) {
|
||||
// Tooltip hover state with timeout for smooth UX
|
||||
const [tooltipOpen, setTooltipOpen] = useState(false);
|
||||
const tooltipTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
@@ -219,4 +221,4 @@ export function GitStatusWidget({ sessionId, isGitRepo, theme, onViewDiff, compa
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -551,6 +551,7 @@ function HamburgerMenuContent({
|
||||
|
||||
// ============================================================================
|
||||
// SessionTooltipContent - Shared tooltip content for session hover previews
|
||||
// PERF: Memoized to prevent re-renders when parent list re-renders
|
||||
// ============================================================================
|
||||
|
||||
interface SessionTooltipContentProps {
|
||||
@@ -561,7 +562,7 @@ interface SessionTooltipContentProps {
|
||||
isInBatch?: boolean; // Whether session is running in auto mode
|
||||
}
|
||||
|
||||
function SessionTooltipContent({
|
||||
const SessionTooltipContent = memo(function SessionTooltipContent({
|
||||
session,
|
||||
theme,
|
||||
gitFileCount,
|
||||
@@ -728,7 +729,7 @@ function SessionTooltipContent({
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Pre-compiled emoji regex for better performance (compiled once at module load)
|
||||
// Matches common emoji patterns at the start of the string including:
|
||||
|
||||
Reference in New Issue
Block a user