Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feature: minimap drag event correction and code refactors #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
utkuakyuz merged 5 commits into main from feature/minimap-drag-event-correction
Aug 28, 2025

Conversation

Copy link
Owner

@utkuakyuz utkuakyuz commented Aug 28, 2025

This PR includes several commits including solution for issue #6 and also major code refactors and code splits on diffMiniMap

Copy link

netlify bot commented Aug 28, 2025
edited
Loading

Deploy Preview for virtual-react-json-diff ready!

Name Link
🔨 Latest commit bddcec5
🔍 Latest deploy log https://app.netlify.com/projects/virtual-react-json-diff/deploys/68b04e4007d71d0008117248
😎 Deploy Preview https://deploy-preview-12--virtual-react-json-diff.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary of Changes

Hello @utkuakyuz, I'm Gemini Code Assist1 ! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces significant architectural improvements to the minimap component by refactoring its core functionalities into two new custom React hooks: useDragScroll and useMinimapDraw. This change addresses an existing issue related to minimap drag event behavior and drastically cleans up the DiffMinimap component by delegating responsibilities, making the codebase more modular and maintainable.

Highlights

  • Minimap Drag Event Correction: The core logic for handling minimap drag events and scrolling has been extracted into a dedicated useDragScroll custom hook. This hook manages mouse interactions (mousedown, mousemove, mouseup) to enable smooth and accurate navigation within the minimap, including corrections for boundary conditions.
  • Minimap Drawing Logic Extraction: All minimap drawing responsibilities, including rendering diff lines, search highlights, and the scrollbox, have been encapsulated within a new useMinimapDraw custom hook. This separation improves modularity and makes the rendering logic more manageable.
  • Major Code Refactoring and Splitting: The DiffMinimap.tsx component has undergone significant refactoring. It now primarily orchestrates the useDragScroll and useMinimapDraw hooks, leading to a cleaner, more readable, and less coupled component structure. This adheres to the principle of separation of concerns.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request does a great job of refactoring the DiffMinimap component by extracting complex logic into custom hooks (useDragScroll and useMinimapDraw). This significantly improves code organization and separation of concerns. My review focuses on a few issues related to React hooks usage, particularly state management for dragging and dependency arrays, which can lead to bugs. Addressing these will make the new implementation more robust and maintainable.

onScroll,
containerRef,
}: UseDragScrollParams): UseDragScrollReturn {
const isDragging = useRef(false);
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot Aug 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The isDragging flag is used to determine the visual state of the minimap's scrollbar. Using useRef for this is incorrect because ref updates do not trigger re-renders. This causes a bug where the scrollbar remains highlighted after a drag operation is completed, as the component does not re-render to reflect the change in isDragging state.

To fix this, isDragging should be managed with useState. This will ensure the component re-renders when the dragging state changes. You will need to update the hook's return type and pass the isDragging state (not a ref) to useMinimapDraw. This change also makes the useEffect on lines 111-118 redundant and it should be removed.

Comment on lines +111 to +118
useEffect(() => {
const handleMouseUp = () => {
isDragging.current = false;
};

window.addEventListener("mouseup", handleMouseUp);
return () => window.removeEventListener("mouseup", handleMouseUp);
}, []);
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot Aug 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This useEffect adds a global mouseup listener to set isDragging.current to false. This is redundant with the logic in handleWindowMouseUp, which already handles cleaning up the dragging state and its associated event listeners.

More importantly, as mentioned in another comment, isDragging should be a state variable. Once that is addressed, this useEffect becomes entirely unnecessary and should be removed to avoid confusion and redundant event listeners.

else {
drawScrollBox(ctx, MINIMAP_HOVER_SCROLL_COLOR);
}
}, [leftDiff, rightDiff, height, currentScrollTop, searchResults, currentMatchIndex, drawLine, viewportHeight]);
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot Aug 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The dependency array for drawMinimap is incorrect. It should depend on drawScrollBox which it calls, and isDragging which determines the color. The current flattened dependency list is incomplete and hard to maintain.

Assuming isDragging is converted to a state variable as per my other comment, the dependency array should be updated to include drawScrollBox and isDragging. This will ensure drawMinimap is re-created and the minimap is redrawn when the dragging state or other drawing-related data changes.

Suggested change
}, [leftDiff, rightDiff, height, currentScrollTop, searchResults, currentMatchIndex, drawLine, viewportHeight]);
}, [drawScrollBox,isDragging,leftDiff, rightDiff, height, currentScrollTop, searchResults, currentMatchIndex, drawLine, viewportHeight]);

}
},
[height, totalLines, viewportHeight, onScroll],
[height, totalLines, viewportHeight, currentScrollTop, drawMinimap],
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot Aug 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The dependency array for handleMouseMove is missing ROW_HEIGHT, which is used on line 78. Although ROW_HEIGHT is currently stable because it's memoized with an empty dependency array, it's important to follow the rules of hooks and include all dependencies. This prevents subtle bugs if ROW_HEIGHT's calculation ever changes.

Suggested change
[height, totalLines, viewportHeight, currentScrollTop, drawMinimap],
[height, totalLines, viewportHeight, currentScrollTop, drawMinimap,ROW_HEIGHT],

@@ -0,0 +1,132 @@
// useMinimapDraw.js
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot Aug 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Minor correction: The filename in the comment is useMinimapDraw.js, but the file extension is .ts.

Suggested change
// useMinimapDraw.js
// useMinimapDraw.ts

Copy link
Owner Author

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request is a great step towards improving code organization by refactoring the minimap logic into custom hooks. The separation of concerns into useDragScroll and useMinimapDraw is a solid architectural choice. However, I've identified some critical issues in the implementation of these hooks, particularly concerning event listener management in useDragScroll, which could lead to memory leaks and bugs. Additionally, there are significant performance problems in the drawing logic within useMinimapDraw that could degrade user experience. My review includes detailed suggestions to address these issues by adopting more idiomatic React patterns and optimizing the drawing pipeline.

Comment on lines 17 to 114
export function useDragScroll({
height,
totalLines,
viewportHeight,
ROW_HEIGHT,
onScroll,
containerRef,
}: UseDragScrollParams): UseDragScrollReturn {
const [isDragging, setIsDragging] = useState(false);

const isValidScroll = useCallback((scrollTop: number): boolean => {
return !Number.isNaN(scrollTop) && Number.isFinite(scrollTop);
}, []);

const calculateScrollFromMouseY = useCallback((relativeY: number): number => {
const viewportCenter = relativeY - viewportHeight / 2;
return (viewportCenter / height) * totalLines * ROW_HEIGHT;
}, [height, totalLines, viewportHeight, ROW_HEIGHT]);

const calculateMaxScroll = useCallback((): number => {
const totalContentHeight = totalLines * ROW_HEIGHT;
return Math.max(0, totalContentHeight - height);
}, [totalLines, ROW_HEIGHT, height]);

// Handle boundary-constrained scrolling
const handleBoundaryScroll = useCallback((e: MouseEvent, rect: DOMRect): void => {
const relativeY = e.clientY - rect.top;
const maxScroll = calculateMaxScroll();

const cursorBottom = e.clientY + viewportHeight / 2;

if (cursorBottom > rect.bottom) {
// This prevents the viewport indicator from going past the minimap bottom
const constrainedRelativeY = rect.bottom - rect.top - viewportHeight / 2;
const constrainedScrollTop = calculateScrollFromMouseY(constrainedRelativeY);

if (isValidScroll(constrainedScrollTop)) {
onScroll(Math.min(maxScroll, Math.max(0, constrainedScrollTop)));
}
}
else {
// Normal scrolling within bounds
const scrollTop = calculateScrollFromMouseY(relativeY);

if (isValidScroll(scrollTop)) {
onScroll(Math.min(maxScroll, Math.max(0, scrollTop)));
}
}
}, [calculateScrollFromMouseY, calculateMaxScroll, isValidScroll, onScroll, viewportHeight]);

// Handle mouse movement during drag
const handleWindowMouseMove = useCallback((e: MouseEvent) => {
if (!isDragging || !containerRef.current)
return;

if (height <= 0 || totalLines <= 0)
return;

const rect = containerRef.current.getBoundingClientRect();
handleBoundaryScroll(e, rect);
}, [
containerRef,
height,
totalLines,
handleBoundaryScroll,
]);

// Handle mouse up - stop dragging and cleanup
const handleWindowMouseUp = useCallback(() => {
setIsDragging(false);
window.removeEventListener("mousemove", handleWindowMouseMove);
window.removeEventListener("mouseup", handleWindowMouseUp);
}, [handleWindowMouseMove]);

// Handle initial mouse down - start dragging
const handleMouseDown = useCallback((e: React.MouseEvent) => {
setIsDragging(true);

window.addEventListener("mousemove", handleWindowMouseMove);
window.addEventListener("mouseup", handleWindowMouseUp);

if (!containerRef.current)
return;

const rect = containerRef.current.getBoundingClientRect();
handleBoundaryScroll(e.nativeEvent, rect);
}, [
handleWindowMouseMove,
handleWindowMouseUp,
containerRef,
handleBoundaryScroll,
]);

return {
handleMouseDown,
isDragging,
};
}
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot Aug 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The current implementation for handling drag events has critical flaws related to event listener management and state closures in React. Adding event listeners inside a useCallback like handleMouseDown and removing them in another can lead to memory leaks if the function references change between adding and removing. Furthermore, handleWindowMouseMove has a stale closure over the isDragging state because it's not included in its dependency array, which will prevent dragging from working at all.

A much safer and more idiomatic React approach is to use a useEffect hook to manage side effects like window event listeners. The effect should be dependent on the isDragging state.

import { useCallback, useState, useEffect } from "react";
type UseDragScrollParams = {
 height: number;
 totalLines: number;
 viewportHeight: number;
 ROW_HEIGHT: number;
 onScroll: (scrollTop: number) => void;
 containerRef: React.RefObject<HTMLDivElement>;
};
type UseDragScrollReturn = {
 handleMouseDown: (e: React.MouseEvent) => void;
 isDragging: boolean;
};
export function useDragScroll({
 height,
 totalLines,
 viewportHeight,
 ROW_HEIGHT,
 onScroll,
 containerRef,
}: UseDragScrollParams): UseDragScrollReturn {
 const [isDragging, setIsDragging] = useState(false);
 const handleBoundaryScroll = useCallback((e: MouseEvent | React.MouseEvent<HTMLDivElement>["nativeEvent"], rect: DOMRect): void => {
 const relativeY = e.clientY - rect.top;
 const maxScroll = Math.max(0, (totalLines * ROW_HEIGHT) - height);
 const cursorBottom = e.clientY + viewportHeight / 2;
 let scrollTop;
 if (cursorBottom > rect.bottom) {
 const constrainedRelativeY = rect.bottom - rect.top - viewportHeight / 2;
 scrollTop = ((constrainedRelativeY - viewportHeight / 2) / height) * totalLines * ROW_HEIGHT;
 } else {
 scrollTop = ((relativeY - viewportHeight / 2) / height) * totalLines * ROW_HEIGHT;
 }
 if (!Number.isNaN(scrollTop) && Number.isFinite(scrollTop)) {
 onScroll(Math.min(maxScroll, Math.max(0, scrollTop)));
 }
 }, [height, totalLines, viewportHeight, ROW_HEIGHT, onScroll]);
 useEffect(() => {
 const handleWindowMouseMove = (e: MouseEvent) => {
 if (!containerRef.current) return;
 const rect = containerRef.current.getBoundingClientRect();
 handleBoundaryScroll(e, rect);
 };
 const handleWindowMouseUp = () => {
 setIsDragging(false);
 };
 if (isDragging) {
 window.addEventListener("mousemove", handleWindowMouseMove);
 window.addEventListener("mouseup", handleWindowMouseUp);
 }
 return () => {
 window.removeEventListener("mousemove", handleWindowMouseMove);
 window.removeEventListener("mouseup", handleWindowMouseUp);
 };
 }, [isDragging, containerRef, handleBoundaryScroll]);
 const handleMouseDown = useCallback((e: React.MouseEvent) => {
 if (!containerRef.current) return;
 
 setIsDragging(true);
 const rect = containerRef.current.getBoundingClientRect();
 handleBoundaryScroll(e.nativeEvent, rect);
 }, [containerRef, handleBoundaryScroll]);
 return {
 handleMouseDown,
 isDragging,
 };
}

Comment on lines 96 to 124
// Draw the scroll box and also differences in minimapo
const drawScrollBox = useCallback((ctx: CanvasRenderingContext2D, color: string) => {
const totalContentHeight = totalLines * ROW_HEIGHT;
const viewportTop = (currentScrollTop / totalContentHeight) * height;

drawDifferencesInMinimap(ctx);

ctx.fillStyle = color;
ctx.fillRect(0, viewportTop, miniMapWidth, viewportHeight);
}, [totalLines, ROW_HEIGHT, currentScrollTop, height, drawDifferencesInMinimap, miniMapWidth, viewportHeight]);

const drawMinimap = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas)
return;

const ctx = canvas.getContext("2d");
if (!ctx)
return;

ctx.clearRect(0, 0, canvas.width, canvas.height);

if (!isDragging) {
drawScrollBox(ctx, MINIMAP_SCROLL_COLOR);
}
else {
drawScrollBox(ctx, MINIMAP_HOVER_SCROLL_COLOR);
}
}, [leftDiff, rightDiff, height, currentScrollTop, searchResults, currentMatchIndex, drawLine, viewportHeight, drawScrollBox, isDragging]);
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot Aug 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current drawing logic is inefficient. drawScrollBox calls drawDifferencesInMinimap, which iterates over all diff lines. In DiffMinimap.tsx, handleMouseMove calls drawMinimap (which calls drawScrollBox) and then potentially drawScrollBox again. This results in redrawing all the diff lines multiple times on every mouse movement, causing performance issues.

The drawing of diffs should be separated from drawing the scrollbar. drawMinimap should be responsible for drawing the diffs once, and then drawing the scrollbar. drawScrollBox should only draw the scrollbar rectangle. This will also simplify the dependency array of drawMinimap.

 // Draw the scroll box
 const drawScrollBox = useCallback((ctx: CanvasRenderingContext2D, color: string) => {
 const totalContentHeight = totalLines * ROW_HEIGHT;
 const viewportTop = (currentScrollTop / totalContentHeight) * height;
 ctx.fillStyle = color;
 ctx.fillRect(0, viewportTop, miniMapWidth, viewportHeight);
 }, [totalLines, ROW_HEIGHT, currentScrollTop, height, miniMapWidth, viewportHeight]);
 const drawMinimap = useCallback(() => {
 const canvas = canvasRef.current;
 if (!canvas)
 return;
 const ctx = canvas.getContext("2d");
 if (!ctx)
 return;
 ctx.clearRect(0, 0, canvas.width, canvas.height);
 drawDifferencesInMinimap(ctx);
 if (!isDragging) {
 drawScrollBox(ctx, MINIMAP_SCROLL_COLOR);
 }
 else {
 drawScrollBox(ctx, MINIMAP_HOVER_SCROLL_COLOR);
 }
 }, [canvasRef, drawDifferencesInMinimap, isDragging, drawScrollBox]);

Comment on lines +126 to +128
useEffect(() => {
drawMinimap();
}, [drawMinimap]);
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot Aug 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This useEffect calls drawMinimap whenever drawMinimap changes. However, the DiffMinimap component, which uses this hook, already has an identical useEffect to call drawMinimap. This results in drawMinimap being called twice for every change, which is redundant and inefficient. The responsibility of when to draw should lie with the component using the hook. This useEffect should be removed.

@utkuakyuz utkuakyuz merged commit 352accd into main Aug 28, 2025
4 checks passed
Copy link
Owner Author

utkuakyuz commented Aug 28, 2025
edited
Loading

The reviews of Gemini needs some rework because directly implementing it brokes the logic.

Right now there is no memory leak nor visual issue. So for quick fix this pull request is merged and closed.

Further work will include code refactors.

Closes #6

@utkuakyuz utkuakyuz deleted the feature/minimap-drag-event-correction branch August 28, 2025 13:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Reviewers
1 more reviewer

@gemini-code-assist gemini-code-assist[bot] gemini-code-assist[bot] left review comments

Reviewers whose approvals may not affect merge requirements
Assignees
No one assigned
Labels
None yet
Projects
None yet
Milestone
No milestone
Development

Successfully merging this pull request may close these issues.

1 participant

AltStyle によって変換されたページ (->オリジナル) /