From 621af28d2f428b3df1dc90ae369b4071cd089e52 Mon Sep 17 00:00:00 2001 From: "ben.elyess@cgm.com" Date: Sun, 5 Jan 2025 13:52:30 +0100 Subject: [PATCH 01/14] add visualisation complexity and search filtring --- README.md | 111 ++++- src/app/page.js | 522 +++++++++++++++--------- src/components/ui/alert.jsx | 31 ++ src/components/ui/filter-dialog.jsx | 64 +++ src/components/ui/mini-map.jsx | 95 +++++ src/components/ui/timeline-controls.jsx | 68 +++ src/lib/analysis.js | 69 ++++ src/lib/utils.js | 53 +++ 8 files changed, 811 insertions(+), 202 deletions(-) create mode 100644 src/components/ui/alert.jsx create mode 100644 src/components/ui/filter-dialog.jsx create mode 100644 src/components/ui/mini-map.jsx create mode 100644 src/components/ui/timeline-controls.jsx create mode 100644 src/lib/analysis.js diff --git a/README.md b/README.md index 14172d4..6c5c133 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,112 @@ -### Code Visualizer: +# Code Timeline Preview -**Transforming Code into Engaging Visual Timelines** +A modern, interactive code visualization tool that helps developers understand and analyze code structure and complexity over time. -Code Visualizer is a web application that generates colorful visualizations from code inputs, highlighting different segments for improved readability and engagement. +## Live Demo + +Try out the live demo at: [codevi.netlify.app](https://codevi.netlify.app/) Thumbnail -Find Code Visualizer at the following URL: +## Features + +### Core Visualization +- **Interactive Timeline**: Visualize code structure with an intuitive timeline interface +- **Syntax Highlighting**: Clear color-coding for different code elements: + - Keywords (Deep Red) + - Classes (Rich Green) + - Functions (Deep Purple) + - Variables (Rich Blue) + - Operators (Warm Orange) + - Strings (Ocean Blue) + - Numbers (Ruby Red) + - Comments (Neutral Gray) + - Decorators (Bright Orange) + +### Code Analysis +- **Complexity Visualization**: Toggle between syntax highlighting and complexity view +- **Complexity Metrics**: + - Low Complexity (Green): Simple, straightforward code + - Medium Complexity (Yellow): Moderate control flow and nesting + - High Complexity (Red): Complex logic and deep nesting +- **Code Structure Analysis**: Automatic analysis of: + - Control flow statements + - Nesting levels + - Logical operators + - Dependencies + +### Navigation & Controls +- **Search Functionality**: Filter code segments based on text search +- **Filter Dialog**: Show/hide specific code types: + - Keywords + - Classes + - Functions + - Variables + - Operators + - Strings + - Numbers + - Comments +- **Zoom Controls**: Adjust timeline view scale +- **Mini-map Navigation**: Quick navigation through large codebases with visual preview +- **Scrolling**: Smooth scrolling with proper viewport management + +### UI Features +- **Dark/Light Mode**: Optimized color schemes for both themes +- **Responsive Layout**: Adapts to different screen sizes +- **Interactive Tooltips**: Detailed information on hover +- **Modern Design**: + - Clean, minimalist interface + - Subtle shadows and transitions + - Professional color palette + - High contrast for readability + +### Editor Integration +- **Code Input**: Built-in code editor with syntax highlighting +- **Real-time Updates**: Immediate visualization of code changes +- **Error Handling**: Validation and error reporting for code input + +## Getting Started + +1. Clone the repository: +```bash +git clone https://github.com/yourusername/code_timeline_preview.git +``` + +2. Install dependencies: +```bash +npm install +``` + +3. Run the development server: +```bash +npm run dev +``` + +4. Open [http://localhost:3000](http://localhost:3000) in your browser + +## Usage + +1. **Input Code**: Paste your code in the left editor panel +2. **Explore Visualization**: + - Use the timeline view on the right + - Toggle between syntax and complexity views + - Use search and filters to focus on specific code elements +3. **Navigate**: + - Use the mini-map for quick navigation + - Zoom in/out to adjust detail level + - Scroll through longer code files + +## Technologies + +- **Frontend**: Next.js, React +- **Styling**: Tailwind CSS +- **Code Editor**: Ace Editor +- **Icons**: Lucide React + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License -[codevi.netlify.com](https://codevi.netlify.app/)
\ No newline at end of file +This project is licensed under the MIT License - see the LICENSE file for details. \ No newline at end of file diff --git a/src/app/page.js b/src/app/page.js index a0e2e01..651491e 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -1,9 +1,15 @@ "use client"; -import React, { useState, useRef } from "react"; -import { Download ,Github} from "lucide-react"; +import React, { useState, useRef, useEffect, useMemo } from "react"; +import { Download, Github, BarChart2 } from "lucide-react"; import html2canvas from "html2canvas"; import AceEditor from "react-ace"; +import { validateCodeInput, parseCodeChanges, getTokenType } from "@/lib/utils"; +import { analyzeCodeSegment, getComplexityColor } from "@/lib/analysis"; +import { Alert } from "@/components/ui/alert"; +import { TimelineControls } from "@/components/ui/timeline-controls"; +import { FilterDialog } from "@/components/ui/filter-dialog"; +import { MiniMap } from "@/components/ui/mini-map"; import "ace-builds/src-noconflict/mode-dart"; import "ace-builds/src-noconflict/theme-dracula"; @@ -14,159 +20,180 @@ const CodeTimeline = () => { const [codeInput, setCodeInput] = useState(""); const [timelineData, setTimelineData] = useState([]); const [darkMode, setDarkMode] = useState(true); - const timelineRef = useRef(null); - const [elementTypes, setElementTypes] = useState({ - keyword: "#FF6B6B", // Soft Red - class: "#4ECDC4", // Teal - function: "#45B7D1", // Sky Blue - variable: "#96CEB4", // Sage Green - operator: darkMode ? "#FFD93D" : "#FFD700", // Soft yellow - string: "#FF8C42", // Soft Orange - number: "#6A0572", // Deep Purple - boolean: "#FF4081", // Pink - comment: "#78909C", // Blue Grey - import: "#26A69A", // Green Teal - decorator: "#BA68C8", // Light Purple - punctuation: "#B0BEC5", // Light Blue Grey - bracket: "#00BCD4", // Cyan - property: "#8BC34A", // Light Green - space: "transparent", - default: darkMode ? "#E0E0E0" : "#424242", // Light Grey / Dark Grey + const [error, setError] = useState(null); + const [zoom, setZoom] = useState(1); + const [searchTerm, setSearchTerm] = useState(""); + const [isFilterOpen, setIsFilterOpen] = useState(false); + const [showComplexity, setShowComplexity] = useState(false); + const [filters, setFilters] = useState({ + keyword: true, + class: true, + function: true, + variable: true, + operator: true, + string: true, + number: true, + boolean: true, + comment: true, + decorator: true, + bracket: true, + punctuation: true, }); + const [visibleRange, setVisibleRange] = useState({ start: 0, end: 20 }); - const keywords = [ - "class", - "function", - "const", - "let", - "var", - "if", - "else", - "for", - "while", - "return", - "import", - "from", - "async", - "await", - "try", - "catch", - "throw", - "new", - "this", - "super", - ]; - - const tokenizeLine = (line) => { - const tokens = line.split(/(\s+|[{}()[\],;.])/); - - return tokens - .map((token) => { - if (!token) return null; + const timelineRef = useRef(null); + const timelineContainerRef = useRef(null); - let color = elementTypes.default; - let width = token.length * 8; + const elementTypes = { + keyword: darkMode ? '#FF7B72' : '#D32F2F', // Deeper red + class: darkMode ? '#7EE787' : '#2E7D32', // Richer green + function: darkMode ? '#D2A8FF' : '#6200EA', // Deeper purple + variable: darkMode ? '#79C0FF' : '#0277BD', // Richer blue + operator: darkMode ? '#FFB757' : '#F57C00', // Warmer orange + string: darkMode ? '#A5D6FF' : '#0277BD', // Ocean blue + number: darkMode ? '#FFA657' : '#C62828', // Ruby red + boolean: darkMode ? '#FF7B72' : '#D32F2F', // Crimson red + comment: darkMode ? '#8B949E' : '#757575', // Neutral gray + decorator: darkMode ? '#FFA657' : '#F57C00', // Bright orange + bracket: darkMode ? '#8B949E' : '#546E7A', // Steel blue-gray + punctuation: darkMode ? '#8B949E' : '#546E7A', // Steel blue-gray + default: darkMode ? '#C9D1D9' : '#24292E', // Default text color + space: 'transparent' + }; - if (token.trim() === "") { - return { - text: token, - color: elementTypes.space, - width: token.length * 8, - }; - } + const handleCodeInput = (value) => { + setCodeInput(value); + setError(null); + try { + validateCodeInput(value); + const lines = parseCodeChanges(value); + const newTimelineData = lines.map((line, index) => { + const analysis = analyzeCodeSegment(line); + return { + id: index + 1, + complexity: analysis.complexity, + segments: line.split(/(\s+|[{}()[\],;.])/) + .map(token => { + if (!token) return null; + + if (token.trim() === "") { + return { + text: token, + color: "transparent", + width: token.length * 8, + }; + } - if (keywords.includes(token)) { - color = elementTypes.keyword; - } else if (token.match(/^[A-Z][a-zA-Z0-9]*$/)) { - color = elementTypes.class; - } else if (token.match(/^[a-z][a-zA-Z0-9]*(?=\()/)) { - color = elementTypes.function; - } else if (token.match(/^[a-z][a-zA-Z0-9]*$/)) { - color = elementTypes.variable; - } else if (token.match(/[+\-*/%=<>!&|^~]/)) { - color = elementTypes.operator; - } else if (token.match(/^(['"]).*\1$/)) { - color = elementTypes.string; - } else if (token.match(/^\d+$/)) { - color = elementTypes.number; - } else if (token === "true" || token === "false") { - color = elementTypes.boolean; - } else if (token.startsWith("//")) { - color = elementTypes.comment; - } else if (token === "import" || token === "from") { - color = elementTypes.import; - } else if (token.startsWith("@")) { - color = elementTypes.decorator; - } else if (token.match(/[{}()[\]]/)) { - color = elementTypes.bracket; - } else if (token.match(/[.,;]/)) { - color = elementTypes.punctuation; - } + const type = getTokenType(token); + return { + text: token, + type, + color: elementTypes[type] || elementTypes.default, + width: token.length * 8, + }; + }) + .filter(Boolean) + }; + }); + setTimelineData(newTimelineData); + } catch (err) { + setError(err.message); + } + }; - return { text: token, color, width }; - }) - .filter(Boolean); + const getSegmentColor = (segment, complexity) => { + if (segment.color === 'transparent') return 'transparent'; + if (showComplexity) { + return getComplexityColor(complexity); + } + return elementTypes[segment.type] || elementTypes.default; }; - const generateTimelineFromCode = (code) => { - const lines = code.split("\n"); - return lines - .map((line, index) => ({ - id: index + 1, - segments: tokenizeLine(line), - })) - .filter((line) => line.segments.length > 0); + const getSegmentTooltip = (segment, complexity) => { + let tooltip = segment.text; + if (showComplexity) { + tooltip += ` (Complexity: ${complexity})`; + tooltip += `\nType: ${segment.type}`; + } + return tooltip; }; - const handleInputChange = (newCode) => { - setCodeInput(newCode); - setTimelineData(generateTimelineFromCode(newCode)); + const handleSearch = (term) => { + setSearchTerm(term); }; - const toggleDarkMode = () => { - setDarkMode(!darkMode); - localStorage.setItem("darkMode", !darkMode); + const handleZoomIn = () => { + setZoom(prev => Math.min(prev + 0.2, 2)); }; - const exportImage = async () => { - if (timelineRef.current) { - const clone = timelineRef.current.cloneNode(true); + const handleZoomOut = () => { + setZoom(prev => Math.max(prev - 0.2, 0.5)); + }; - document.body.appendChild(clone); + const handleFilterChange = (newFilters) => { + setFilters(newFilters); + }; - const { scrollWidth, scrollHeight } = clone; + const filteredTimelineData = useMemo(() => { + return timelineData + .map(row => ({ + ...row, + segments: row.segments.filter(segment => { + const type = segment.type; + return filters[type] && + (!searchTerm || segment.text.toLowerCase().includes(searchTerm.toLowerCase())); + }) + })) + .filter(row => row.segments.length > 0); + }, [timelineData, filters, searchTerm]); - const canvas = await html2canvas(clone, { - width: 922, - height: scrollHeight, + const downloadImage = async () => { + if (!timelineRef.current) return; + + try { + const canvas = await html2canvas(timelineRef.current, { backgroundColor: darkMode ? "#2D2D2D" : "#FFFFFF", scale: window.devicePixelRatio, }); - - document.body.removeChild(clone); - - const image = canvas - .toDataURL("image/png") - .replace("image/png", "image/octet-stream"); + const image = canvas.toDataURL("image/png"); const link = document.createElement("a"); - link.download = "code-timeline.png"; link.href = image; + link.download = "code-timeline.png"; link.click(); + } catch (err) { + setError("Failed to download image: " + err.message); } }; - // useEffect(() => { - // const darkModeSetting = localStorage.getItem("darkMode"); - // const isDarkMode = darkModeSetting === "true" || darkModeSetting === null; - // setDarkMode(isDarkMode); + const handleScroll = () => { + if (!timelineContainerRef.current) return; + + const container = timelineContainerRef.current; + const { scrollTop, clientHeight, scrollHeight } = container; + + // Calculate visible lines based on scroll position and container height + const totalLines = timelineData.length; + const lineHeight = scrollHeight / totalLines; + + const start = Math.floor(scrollTop / lineHeight); + const end = Math.min(Math.ceil((scrollTop + clientHeight) / lineHeight), totalLines); + + setVisibleRange({ start, end }); + }; + + useEffect(() => { + const container = timelineContainerRef.current; + if (container) { + container.addEventListener('scroll', handleScroll); + // Initial calculation + handleScroll(); + return () => container.removeEventListener('scroll', handleScroll); + } + }, [timelineData.length]); - // setElementTypes((prev) => ({ - // ...prev, - // operator: !isDarkMode ? "#FFD93D" : "#FFD700", - // default: !isDarkMode ? "#E0E0E0" : "#424242", - // })); - // setTimelineData(generateTimelineFromCode(codeInput)); - // }, [codeInput, darkMode]); + useEffect(() => { + handleScroll(); + }, [zoom]); return (
@@ -179,18 +206,19 @@ const CodeTimeline = () => { Code Timeline Visualizer
- {/*
- - {darkMode ? ( - - ) : ( - - )} - -
*/} + {
-

- See your code come to life! -

+ {error && ( + setError(null)} + className="mb-4" + /> + )}
@@ -229,7 +258,7 @@ const CodeTimeline = () => { placeholder="Paste your code here..." theme={darkMode ? "dracula" : "github"} value={codeInput} - mode={"dart"} + mode="dart" width="100%" showPrintMargin={false} showGutter={false} @@ -237,77 +266,176 @@ const CodeTimeline = () => { height="100%" setOptions={{ fontSize: "16px", + fontFamily: "'JetBrains Mono', monospace", }} - onChange={handleInputChange} + onChange={handleCodeInput} + className={`rounded-lg border ${ + darkMode + ? "border-gray-700" + : "border-gray-200 shadow-sm" + }`} />
-
-
- {timelineData.map((row) => ( -
- - {row.id} - -
- {row.segments.map((segment, segIndex) => ( -
- ))} +
+ setIsFilterOpen(true)} + darkMode={darkMode} + /> + +
+ +
+
+
+ {filteredTimelineData.map((row) => ( +
+ + {row.id} + +
+ {row.segments.map((segment, segIndex) => ( +
+ +
+ ))} +
-
- ))} + ))} +
+ + { + if (timelineContainerRef.current) { + const scrollHeight = timelineContainerRef.current.scrollHeight; + const scrollPosition = (position / timelineData.length) * scrollHeight; + timelineContainerRef.current.scrollTop = scrollPosition; + } + }} + darkMode={darkMode} + />
- {Object.entries(elementTypes).map( - ([key, color]) => - key !== "space" && - key !== "default" && ( -
-
- - {key.charAt(0).toUpperCase() + key.slice(1)} - -
- ) + {showComplexity ? ( +
+
+
+ + Low Complexity + +
+
+
+ + Medium Complexity + +
+
+
+ + High Complexity + +
+
+ ) : ( + Object.entries(elementTypes).map( + ([key, color]) => + key !== "space" && + key !== "default" && ( +
+
+ + {key.charAt(0).toUpperCase() + key.slice(1)} + +
+ ) + ) )}
+ + setIsFilterOpen(false)} + onApply={handleFilterChange} + filters={filters} + darkMode={darkMode} + />
); }; diff --git a/src/components/ui/alert.jsx b/src/components/ui/alert.jsx new file mode 100644 index 0000000..534a30b --- /dev/null +++ b/src/components/ui/alert.jsx @@ -0,0 +1,31 @@ +"use client"; + +import React from 'react'; +import { cn } from '@/lib/utils'; + +export function Alert({ type = 'info', message, className, onClose }) { + const baseStyles = 'p-4 mb-4 rounded-lg flex justify-between items-center'; + const typeStyles = { + error: 'bg-red-100 text-red-800 dark:bg-red-200 dark:text-red-900', + success: 'bg-green-100 text-green-800 dark:bg-green-200 dark:text-green-900', + warning: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-200 dark:text-yellow-900', + info: 'bg-blue-100 text-blue-800 dark:bg-blue-200 dark:text-blue-900', + }; + + return ( +
+ {message} + {onClose && ( + + )} +
+ ); +} diff --git a/src/components/ui/filter-dialog.jsx b/src/components/ui/filter-dialog.jsx new file mode 100644 index 0000000..8f02f44 --- /dev/null +++ b/src/components/ui/filter-dialog.jsx @@ -0,0 +1,64 @@ +"use client"; + +import React from 'react'; + +export function FilterDialog({ + isOpen, + onClose, + onApply, + filters, + darkMode +}) { + if (!isOpen) return null; + + return ( +
+
+
+

+ Filter Timeline +

+
+ +
+ {Object.entries(filters).map(([key, value]) => ( + + ))} +
+ +
+ +
+
+
+ ); +} diff --git a/src/components/ui/mini-map.jsx b/src/components/ui/mini-map.jsx new file mode 100644 index 0000000..4e7a677 --- /dev/null +++ b/src/components/ui/mini-map.jsx @@ -0,0 +1,95 @@ +"use client"; + +import React, { useEffect, useRef } from 'react'; + +export function MiniMap({ + timelineData, + visibleRange, + onNavigate, + darkMode, + width = 150, + height = 100 +}) { + const canvasRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas || !timelineData.length) return; + + const ctx = canvas.getContext('2d'); + const totalLines = timelineData.length; + + // Clear canvas + ctx.fillStyle = darkMode ? '#1f2937' : '#f9fafb'; + ctx.fillRect(0, 0, width, height); + + // Calculate dimensions + const lineHeight = height / totalLines; + + // Draw timeline segments + timelineData.forEach((row, index) => { + const y = (index / totalLines) * height; + + // Draw background for the entire line + ctx.fillStyle = darkMode ? '#374151' : '#e5e7eb'; + ctx.fillRect(0, y, width, Math.max(1, lineHeight)); + + // Draw segments + let currentX = 0; + row.segments.forEach(segment => { + if (segment.color !== 'transparent') { + const segmentWidth = (segment.width / 8) * (width / 100); + ctx.fillStyle = segment.color; + ctx.fillRect(currentX, y, Math.max(1, segmentWidth), Math.max(1, lineHeight)); + currentX += segmentWidth; + } + }); + }); + + // Draw visible range indicator + if (visibleRange) { + const { start, end } = visibleRange; + const visibleStart = (start / totalLines) * height; + const visibleHeight = ((end - start) / totalLines) * height; + + // Draw semi-transparent overlay for non-visible areas + ctx.fillStyle = `rgba(0, 0, 0, ${darkMode ? 0.5 : 0.2})`; + ctx.fillRect(0, 0, width, visibleStart); + ctx.fillRect(0, visibleStart + visibleHeight, width, height - (visibleStart + visibleHeight)); + + // Draw border around visible area + ctx.strokeStyle = darkMode ? '#60a5fa' : '#3b82f6'; + ctx.lineWidth = 2; + ctx.strokeRect(0, visibleStart, width, visibleHeight); + } + }, [timelineData, visibleRange, darkMode, width, height]); + + const handleClick = (e) => { + if (!timelineData.length) return; + + const rect = canvasRef.current.getBoundingClientRect(); + const y = e.clientY - rect.top; + const clickedPosition = Math.floor((y / height) * timelineData.length); + + onNavigate(Math.max(0, Math.min(clickedPosition, timelineData.length - 1))); + }; + + return ( +
+ +
+ ); +} diff --git a/src/components/ui/timeline-controls.jsx b/src/components/ui/timeline-controls.jsx new file mode 100644 index 0000000..515bbbc --- /dev/null +++ b/src/components/ui/timeline-controls.jsx @@ -0,0 +1,68 @@ +"use client"; + +import React from 'react'; +import { Search, ZoomIn, ZoomOut, Filter } from 'lucide-react'; + +export function TimelineControls({ + onZoomIn, + onZoomOut, + onSearch, + onFilter, + darkMode +}) { + return ( +
+
+ onSearch(e.target.value)} + className={`w-full pl-10 pr-4 py-2 rounded-lg border ${ + darkMode + ? 'bg-gray-800 border-gray-700 text-gray-200' + : 'bg-white border-gray-200 text-gray-700' + }`} + /> + +
+ + + + + + +
+ ); +} diff --git a/src/lib/analysis.js b/src/lib/analysis.js new file mode 100644 index 0000000..9f65f59 --- /dev/null +++ b/src/lib/analysis.js @@ -0,0 +1,69 @@ +// Code complexity analysis utilities + +/** + * Calculate cognitive complexity of a code segment + * This is a simplified version that looks for common complexity indicators + */ +export function calculateComplexity(code) { + let score = 0; + + // Control flow statements + const controlFlow = (code.match(/if|else|for|while|do|switch|case|try|catch|finally/g) || []).length; + score += controlFlow * 2; + + // Nesting level + const nesting = (code.match(/{/g) || []).length; + score += nesting; + + // Logical operators + const logicalOps = (code.match(/&&|\|\||!(?!=)/g) || []).length; + score += logicalOps; + + // Ternary operators + const ternary = (code.match(/\?.*:/g) || []).length; + score += ternary * 2; + + return score; +} + +/** + * Analyze code structure to find dependencies between different parts + */ +export function analyzeDependencies(code) { + const imports = (code.match(/import.*from|require\(.*\)/g) || []).length; + return imports; +} + +/** + * Calculate the relative size score of a code segment + */ +export function calculateSizeScore(code) { + const lines = code.split('\n').length; + const characters = code.length; + return Math.log(lines * characters + 1); +} + +/** + * Generate a complexity color based on the complexity score + */ +export function getComplexityColor(complexity) { + if (complexity <= 2) { + return '#4CAF50'; // Low complexity - Green + } else if (complexity <= 5) { + return '#FFC107'; // Medium complexity - Yellow + } else { + return '#F44336'; // High complexity - Red + } +} + +/** + * Generate metrics for a code segment + */ +export function analyzeCodeSegment(code) { + const complexity = calculateComplexity(code); + return { + complexity, + dependencies: analyzeDependencies(code), + sizeScore: calculateSizeScore(code) + }; +} diff --git a/src/lib/utils.js b/src/lib/utils.js index b20bf01..5ce74b1 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -4,3 +4,56 @@ import { twMerge } from "tailwind-merge" export function cn(...inputs) { return twMerge(clsx(inputs)); } + +export const validateCodeInput = (input) => { + if (!input || typeof input !== 'string') { + throw new Error('Input must be a non-empty string'); + } + + // Basic code structure validation + const lines = input.split('\n'); + if (lines.length === 0) { + throw new Error('Input must contain at least one line of code'); + } + + return true; +}; + +export const parseCodeChanges = (input) => { + try { + // Remove any BOM and normalize line endings + const normalizedInput = input.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n'); + + // Split into lines and filter out empty lines + return normalizedInput.split('\n'); + } catch (error) { + throw new Error(`Failed to parse code changes: ${error.message}`); + } +}; + +export const getTokenType = (token) => { + if (!token) return 'default'; + + const patterns = { + keyword: /^(class|function|const|let|var|if|else|for|while|return|import|from|async|await|try|catch|throw|new|this|super)$/, + class: /^[A-Z][a-zA-Z0-9]*$/, + function: /^[a-z][a-zA-Z0-9]*(?=\()/, + variable: /^[a-z][a-zA-Z0-9]*$/, + operator: /[+\-*/%=<>!&|^~]/, + string: /^(['"]).*\1$/, + number: /^\d+$/, + boolean: /^(true|false)$/, + comment: /^\/\//, + decorator: /^@/, + bracket: /[{}()[\]]/, + punctuation: /[.,;]/ + }; + + for (const [type, pattern] of Object.entries(patterns)) { + if (pattern.test(token)) { + return type; + } + } + + return 'default'; +}; From ca4b8af9c84bf5cb36ecc0b7ffc170fb36f3fe3f Mon Sep 17 00:00:00 2001 From: "ben.elyess@cgm.com" Date: Sun, 5 Jan 2025 14:49:28 +0100 Subject: [PATCH 02/14] fix positioning of infos --- src/app/page.js | 311 +++++++++++++++++++++++++++++++------------- src/lib/analysis.js | 148 ++++++++++++++++++--- 2 files changed, 351 insertions(+), 108 deletions(-) diff --git a/src/app/page.js b/src/app/page.js index 651491e..987e506 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -1,7 +1,7 @@ "use client"; import React, { useState, useRef, useEffect, useMemo } from "react"; -import { Download, Github, BarChart2 } from "lucide-react"; +import { Download, Github, BarChart2, MinusCircle, PlusCircle, Activity } from "lucide-react"; import html2canvas from "html2canvas"; import AceEditor from "react-ace"; import { validateCodeInput, parseCodeChanges, getTokenType } from "@/lib/utils"; @@ -40,6 +40,7 @@ const CodeTimeline = () => { punctuation: true, }); const [visibleRange, setVisibleRange] = useState({ start: 0, end: 20 }); + const [tooltipPosition, setTooltipPosition] = useState({ top: true }); const timelineRef = useRef(null); const timelineContainerRef = useRef(null); @@ -61,6 +62,29 @@ const CodeTimeline = () => { space: 'transparent' }; + const getSegmentHeight = (complexity) => { + // Scale height based on complexity + const baseHeight = 16; + return Math.min(baseHeight + (complexity * 2), 40); + }; + + const getSegmentColor = (segment, analysis) => { + if (segment.color === 'transparent') return 'transparent'; + + if (showComplexity) { + return analysis.color; + } + return segment.color; + }; + + const getSegmentOpacity = (analysis) => { + if (showComplexity) { + // Scale opacity with complexity + return Math.min(0.4 + (analysis.complexity * 0.1), 1); + } + return 1; + }; + const handleCodeInput = (value) => { setCodeInput(value); setError(null); @@ -101,23 +125,6 @@ const CodeTimeline = () => { } }; - const getSegmentColor = (segment, complexity) => { - if (segment.color === 'transparent') return 'transparent'; - if (showComplexity) { - return getComplexityColor(complexity); - } - return elementTypes[segment.type] || elementTypes.default; - }; - - const getSegmentTooltip = (segment, complexity) => { - let tooltip = segment.text; - if (showComplexity) { - tooltip += ` (Complexity: ${complexity})`; - tooltip += `\nType: ${segment.type}`; - } - return tooltip; - }; - const handleSearch = (term) => { setSearchTerm(term); }; @@ -181,6 +188,15 @@ const CodeTimeline = () => { setVisibleRange({ start, end }); }; + const handleTooltipPosition = (e, tooltip) => { + const rect = tooltip.getBoundingClientRect(); + const viewportHeight = window.innerHeight; + const spaceAbove = rect.top; + const spaceBelow = viewportHeight - rect.bottom; + + setTooltipPosition({ top: spaceBelow < 100 && spaceAbove > spaceBelow }); + }; + useEffect(() => { const container = timelineContainerRef.current; if (container) { @@ -258,45 +274,82 @@ const CodeTimeline = () => { placeholder="Paste your code here..." theme={darkMode ? "dracula" : "github"} value={codeInput} - mode="dart" + mode="javascript" width="100%" - showPrintMargin={false} - showGutter={false} - highlightActiveLine={false} height="100%" - setOptions={{ - fontSize: "16px", - fontFamily: "'JetBrains Mono', monospace", - }} onChange={handleCodeInput} - className={`rounded-lg border ${ + className={`rounded-lg shadow-sm ${ darkMode - ? "border-gray-700" - : "border-gray-200 shadow-sm" + ? "border border-gray-700" + : "border border-gray-200" }`} + setOptions={{ + showLineNumbers: true, + showGutter: true, + fontSize: 14, + tabSize: 2, + useWorker: false + }} />
-
- setIsFilterOpen(true)} - darkMode={darkMode} - /> - +
+
+ + + {Math.round(zoom * 100)}% + + +
+ +
+ + +
@@ -310,46 +363,119 @@ const CodeTimeline = () => { >
- {filteredTimelineData.map((row) => ( -
- - {row.id} - -
- {row.segments.map((segment, segIndex) => ( -
- -
- ))} + {filteredTimelineData.map((row) => { + const analysis = analyzeCodeSegment(row.segments.map(s => s.text).join('')); + return ( +
+ + {row.id} + +
+ {row.segments.map((segment, segIndex) => ( +
{ + const tooltip = e.currentTarget.querySelector('.segment-tooltip'); + if (tooltip) { + tooltip.style.display = 'block'; + } + }} + onMouseLeave={(e) => { + const tooltip = e.currentTarget.querySelector('.segment-tooltip'); + if (tooltip) { + tooltip.style.display = 'none'; + } + }} + > +
+ + ))} +
-
- ))} + ); + })}
@@ -367,13 +493,11 @@ const CodeTimeline = () => { />
-
+
{showComplexity ? (
@@ -398,12 +522,21 @@ const CodeTimeline = () => {
High Complexity
+
+
+ + Very High Complexity + +
) : ( Object.entries(elementTypes).map( diff --git a/src/lib/analysis.js b/src/lib/analysis.js index 9f65f59..316ca80 100644 --- a/src/lib/analysis.js +++ b/src/lib/analysis.js @@ -1,69 +1,179 @@ // Code complexity analysis utilities +// Code Analysis Constants +const COMPLEXITY_WEIGHTS = { + CONTROL_FLOW: 2, // if, for, while, etc. + NESTING: 1.5, // Nested blocks + LOGICAL_OPS: 1, // &&, ||, ! + TERNARY: 1.5, // ? : + FUNCTION_CALLS: 1, // Function invocations + RECURSION: 2, // Recursive calls +}; + +const CODE_SMELL_PATTERNS = { + LONG_LINE: { pattern: /.{120,}/, message: 'Line too long (>120 characters)', severity: 'warning' }, + NESTED_CALLBACKS: { pattern: /callback.*callback/i, message: 'Nested callbacks detected', severity: 'warning' }, + MAGIC_NUMBERS: { pattern: /(? call.slice(0, -1)); + + return { + imports: imports.length, + exports: exports.length, + functionCalls, + totalDependencies: imports.length + exports.length + functionCalls.length + }; } /** - * Calculate the relative size score of a code segment + * Calculate size score based on code length and structure */ export function calculateSizeScore(code) { const lines = code.split('\n').length; - const characters = code.length; - return Math.log(lines * characters + 1); + const chars = code.length; + return Math.log10(lines * chars) / 2; } /** - * Generate a complexity color based on the complexity score + * Get color based on complexity score */ export function getComplexityColor(complexity) { - if (complexity <= 2) { + if (complexity <= 3) { return '#4CAF50'; // Low complexity - Green - } else if (complexity <= 5) { + } else if (complexity <= 6) { return '#FFC107'; // Medium complexity - Yellow + } else if (complexity <= 9) { + return '#FF9800'; // High complexity - Orange } else { - return '#F44336'; // High complexity - Red + return '#F44336'; // Very high complexity - Red } } /** - * Generate metrics for a code segment + * Detect code smells + */ +export function detectCodeSmells(code) { + const smells = []; + + Object.entries(CODE_SMELL_PATTERNS).forEach(([type, { pattern, message, severity }]) => { + if (pattern.test(code)) { + smells.push({ type, message, severity }); + } + }); + + return smells; +} + +/** + * Analyze performance impact + */ +export function analyzePerformanceImpact(code) { + const impacts = []; + + Object.entries(PERFORMANCE_PATTERNS).forEach(([type, { pattern, message, impact }]) => { + if (pattern.test(code)) { + impacts.push({ type, message, impact }); + } + }); + + return impacts; +} + +/** + * Calculate change impact score + */ +export function calculateChangeImpact(code) { + const complexity = calculateComplexity(code); + const { totalDependencies } = analyzeDependencies(code); + const sizeScore = calculateSizeScore(code); + + return { + score: (complexity * 0.4) + (totalDependencies * 0.4) + (sizeScore * 0.2), + riskLevel: complexity > 7 || totalDependencies > 5 ? 'high' : + complexity > 4 || totalDependencies > 3 ? 'medium' : 'low' + }; +} + +/** + * Main analysis function for code segments */ export function analyzeCodeSegment(code) { const complexity = calculateComplexity(code); + const dependencies = analyzeDependencies(code); + const sizeScore = calculateSizeScore(code); + const codeSmells = detectCodeSmells(code); + const performanceImpact = analyzePerformanceImpact(code); + const changeImpact = calculateChangeImpact(code); + return { complexity, - dependencies: analyzeDependencies(code), - sizeScore: calculateSizeScore(code) + dependencies, + sizeScore, + codeSmells, + performanceImpact, + changeImpact, + color: getComplexityColor(complexity) }; } From d96bf233dd189016973ed491155a45ec8c41cb8d Mon Sep 17 00:00:00 2001 From: "ben.elyess@cgm.com" Date: Sun, 5 Jan 2025 15:14:53 +0100 Subject: [PATCH 03/14] add upload file botton --- src/app/page.js | 45 ++++++++++++++++++++++++------- src/components/ui/file-upload.jsx | 30 +++++++++++++++++++++ 2 files changed, 65 insertions(+), 10 deletions(-) create mode 100644 src/components/ui/file-upload.jsx diff --git a/src/app/page.js b/src/app/page.js index 987e506..666e7f1 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -10,6 +10,7 @@ import { Alert } from "@/components/ui/alert"; import { TimelineControls } from "@/components/ui/timeline-controls"; import { FilterDialog } from "@/components/ui/filter-dialog"; import { MiniMap } from "@/components/ui/mini-map"; +import { FileUpload } from "@/components/ui/file-upload"; import "ace-builds/src-noconflict/mode-dart"; import "ace-builds/src-noconflict/theme-dracula"; @@ -256,6 +257,15 @@ const CodeTimeline = () => { > + +
@@ -270,6 +280,31 @@ const CodeTimeline = () => {
+
+
+ setSearchTerm(e.target.value)} + className={`flex-1 px-4 py-2 rounded border ${ + darkMode + ? "bg-gray-800 border-gray-700 text-gray-200" + : "bg-white border-gray-300" + }`} + /> + +
+
{ > -
diff --git a/src/components/ui/file-upload.jsx b/src/components/ui/file-upload.jsx new file mode 100644 index 0000000..c3969c1 --- /dev/null +++ b/src/components/ui/file-upload.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Upload } from 'lucide-react'; + +export function FileUpload({ onFileContent, className = '' }) { + const handleFileChange = async (e) => { + if (e.target.files && e.target.files[0]) { + const file = e.target.files[0]; + const reader = new FileReader(); + reader.onload = async (e) => { + const text = e.target.result; + onFileContent(text); + }; + reader.readAsText(file); + } + }; + + return ( + + ); +} From 54d5a9ad73a022db6ea6c72beb5fcf6ed4a43d72 Mon Sep 17 00:00:00 2001 From: "ben.elyess@cgm.com" Date: Sun, 5 Jan 2025 16:00:28 +0100 Subject: [PATCH 04/14] fix code smell --- src/app/page.js | 25 +++--- src/components/ui/code-smells.jsx | 96 +++++++++++++++++++++++ src/lib/analysis.js | 125 ++++++++++++++++++++++++++++-- 3 files changed, 226 insertions(+), 20 deletions(-) create mode 100644 src/components/ui/code-smells.jsx diff --git a/src/app/page.js b/src/app/page.js index 666e7f1..7a1e09b 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -42,9 +42,11 @@ const CodeTimeline = () => { }); const [visibleRange, setVisibleRange] = useState({ start: 0, end: 20 }); const [tooltipPosition, setTooltipPosition] = useState({ top: true }); + const [tooltipVisible, setTooltipVisible] = useState(false); const timelineRef = useRef(null); const timelineContainerRef = useRef(null); + const tooltipRef = useRef(null); const elementTypes = { keyword: darkMode ? '#FF7B72' : '#D32F2F', // Deeper red @@ -412,13 +414,13 @@ const CodeTimeline = () => { key={segIndex} className="relative hover:z-10" onMouseEnter={(e) => { - const tooltip = e.currentTarget.querySelector('.segment-tooltip'); + const tooltip = e.currentTarget.querySelector('.tooltip'); if (tooltip) { tooltip.style.display = 'block'; } }} onMouseLeave={(e) => { - const tooltip = e.currentTarget.querySelector('.segment-tooltip'); + const tooltip = e.currentTarget.querySelector('.tooltip'); if (tooltip) { tooltip.style.display = 'none'; } @@ -437,18 +439,17 @@ const CodeTimeline = () => { }} /> diff --git a/src/components/ui/code-smells.jsx b/src/components/ui/code-smells.jsx new file mode 100644 index 0000000..487c4eb --- /dev/null +++ b/src/components/ui/code-smells.jsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { AlertTriangle, AlertCircle, Info } from 'lucide-react'; + +export function CodeSmells({ smells, darkMode }) { + if (!smells || smells.length === 0) { + return ( +
+ No code smells detected +
+ ); + } + + const getSeverityIcon = (severity) => { + switch (severity) { + case 'error': + return ; + case 'warning': + return ; + default: + return ; + } + }; + + const getSeverityColor = (severity) => { + switch (severity) { + case 'error': + return 'text-red-500 bg-red-500/10 border-red-500/20'; + case 'warning': + return 'text-yellow-500 bg-yellow-500/10 border-yellow-500/20'; + default: + return 'text-blue-500 bg-blue-500/10 border-blue-500/20'; + } + }; + + // Group smells by severity + const groupedSmells = smells.reduce((acc, smell) => { + acc[smell.severity] = acc[smell.severity] || []; + acc[smell.severity].push(smell); + return acc; + }, {}); + + const severityOrder = ['error', 'warning', 'info']; + + return ( +
+ {severityOrder.map(severity => + groupedSmells[severity] && ( +
+

+ {getSeverityIcon(severity)} + {severity.charAt(0).toUpperCase() + severity.slice(1)}s + + {groupedSmells[severity].length} + +

+
+ {groupedSmells[severity].map((smell, index) => ( +
+
+ {smell.type.split('_').map(word => + word.charAt(0) + word.slice(1).toLowerCase() + ).join(' ')} +
+

+ {smell.message} +

+ {smell.line && ( +
+ Line {smell.line} +
+ )} + {smell.code && ( +
+                      {smell.code}
+                    
+ )} +
+ ))} +
+
+ ) + )} +
+ ); +} diff --git a/src/lib/analysis.js b/src/lib/analysis.js index 316ca80..a89909f 100644 --- a/src/lib/analysis.js +++ b/src/lib/analysis.js @@ -11,11 +11,87 @@ const COMPLEXITY_WEIGHTS = { }; const CODE_SMELL_PATTERNS = { - LONG_LINE: { pattern: /.{120,}/, message: 'Line too long (>120 characters)', severity: 'warning' }, - NESTED_CALLBACKS: { pattern: /callback.*callback/i, message: 'Nested callbacks detected', severity: 'warning' }, - MAGIC_NUMBERS: { pattern: /(? { + Object.entries(CODE_SMELL_PATTERNS).forEach(([type, { pattern, message, severity }]) => { + if (pattern.test(line)) { + smells.push({ + type, + message, + severity, + line: lineNumber + 1, + code: line.trim() + }); + } + }); + }); + + // Check entire code block for multi-line patterns Object.entries(CODE_SMELL_PATTERNS).forEach(([type, { pattern, message, severity }]) => { if (pattern.test(code)) { - smells.push({ type, message, severity }); + // Avoid duplicate reports for patterns that were already caught line by line + const alreadyReported = smells.some(smell => + smell.type === type && smell.code === code.match(pattern)?.[0]?.trim() + ); + + if (!alreadyReported) { + smells.push({ + type, + message, + severity, + code: code.match(pattern)?.[0]?.trim() || '' + }); + } } }); - return smells; + // Sort by severity (error > warning > info) + return smells.sort((a, b) => { + const severityOrder = { error: 0, warning: 1, info: 2 }; + return severityOrder[a.severity] - severityOrder[b.severity]; + }); } /** From eee32f8ecf5d8c52a96b162af9c439a7348fc4be Mon Sep 17 00:00:00 2001 From: "ben.elyess@cgm.com" Date: Sun, 5 Jan 2025 16:03:07 +0100 Subject: [PATCH 05/14] fix --- src/lib/analysis.js | 257 +++++++++++++++++++++++++++++++++----------- 1 file changed, 192 insertions(+), 65 deletions(-) diff --git a/src/lib/analysis.js b/src/lib/analysis.js index a89909f..032a312 100644 --- a/src/lib/analysis.js +++ b/src/lib/analysis.js @@ -112,6 +112,181 @@ const PERFORMANCE_PATTERNS = { } }; +// Language-specific code smell rules +const languageRules = { + javascript: { + patterns: [ + { regex: /console\.(log|warn|error|info|debug)/g, message: "Unexpected console statement" }, + { regex: /var\s/g, message: "Use let/const instead of var" }, + { regex: /==(?!=)/g, message: "Use === instead of ==" }, + { regex: /\.length\s*===?\s*0/g, message: "Use .length > 0 instead of === 0" }, + { regex: /for\s*\(.*\{[\s\S]*\}/g, message: "Consider using array methods instead of for loops" }, + { regex: /catch\s*\(\s*e\s*\)/g, message: "Use more descriptive error variable names" }, + { regex: /setTimeout\s*\(\s*function\s*\(\)/g, message: "Consider using async/await instead of setTimeout" } + ] + }, + python: { + patterns: [ + { regex: /print\s*\([^)]*\)/g, message: "Consider using logging instead of print" }, + { regex: /except:/g, message: "Avoid bare except clause" }, + { regex: /import \*/g, message: "Avoid wildcard imports" }, + { regex: /global\s+[a-zA-Z_]/g, message: "Avoid global variables" }, + { regex: /lambda/g, message: "Consider using a regular function instead of lambda" } + ] + }, + java: { + patterns: [ + { regex: /System\.out\.println/g, message: "Use a logger instead of System.out.println" }, + { regex: /catch\s*\(\s*Exception\s+e\s*\)/g, message: "Avoid catching generic Exception" }, + { regex: /null\s*==/g, message: "Use Objects.isNull() or Optional" }, + { regex: /synchronized/g, message: "Consider using concurrent collections instead" } + ] + }, + csharp: { + patterns: [ + { regex: /Console\.(Write|WriteLine)/g, message: "Use logging framework instead of Console" }, + { regex: /catch\s*\(\s*Exception\s+e\s*\)/g, message: "Avoid catching generic Exception" }, + { regex: /goto/g, message: "Avoid using goto statements" } + ] + }, + ruby: { + patterns: [ + { regex: /puts/g, message: "Use Rails.logger instead of puts" }, + { regex: /rescue\s*$/g, message: "Avoid rescuing without specifying an error class" }, + { regex: /eval/g, message: "Avoid using eval" } + ] + }, + php: { + patterns: [ + { regex: /var_dump|print_r/g, message: "Use proper logging instead of debug functions" }, + { regex: /\$_GET|\$_POST/g, message: "Validate input data before usage" }, + { regex: /mysql_/g, message: "Use PDO or mysqli instead of mysql_* functions" } + ] + }, + go: { + patterns: [ + { regex: /panic\(/g, message: "Avoid using panic" }, + { regex: /\.Error\(\)\s*==\s*""/g, message: "Check error type instead of error string" }, + { regex: /time.Sleep/g, message: "Consider using contexts for timeouts" } + ] + }, + rust: { + patterns: [ + { regex: /unwrap\(\)/g, message: "Handle Result/Option explicitly instead of unwrap" }, + { regex: /panic!\(/g, message: "Avoid using panic!" }, + { regex: /unsafe\s*\{/g, message: "Minimize usage of unsafe blocks" } + ] + }, + swift: { + patterns: [ + { regex: /print\(/g, message: "Use logging framework instead of print" }, + { regex: /try\!/g, message: "Avoid force try" }, + { regex: /as\!/g, message: "Avoid force casting" } + ] + }, + kotlin: { + patterns: [ + { regex: /println\(/g, message: "Use logging framework instead of println" }, + { regex: /!!/g, message: "Avoid using not-null assertion operator" }, + { regex: /lateinit/g, message: "Consider using nullable or lazy properties" } + ] + }, + typescript: { + patterns: [ + { regex: /any/g, message: "Avoid using 'any' type" }, + { regex: /console\.(log|warn|error|info|debug)/g, message: "Unexpected console statement" }, + { regex: /\!=/g, message: "Use !== instead of !=" } + ] + } +}; + +// Detect language based on code content and file extension +function detectLanguage(code, fileExtension = '') { + // Map file extensions to languages + const extensionMap = { + js: 'javascript', + jsx: 'javascript', + ts: 'typescript', + tsx: 'typescript', + py: 'python', + java: 'java', + cs: 'csharp', + rb: 'ruby', + php: 'php', + go: 'go', + rs: 'rust', + swift: 'swift', + kt: 'kotlin' + }; + + // Try to detect from extension first + if (fileExtension && extensionMap[fileExtension.toLowerCase()]) { + return extensionMap[fileExtension.toLowerCase()]; + } + + // Fallback to content-based detection + const languagePatterns = { + python: /(def|import|from|class|if __name__ == ['"]__main__['"]:)/, + javascript: /(const|let|var|function|=>|require\(|import\s+.*\s+from)/, + java: /(public class|private|protected|package|import java)/, + csharp: /(using System|namespace|public class|private|protected)/, + ruby: /(require|def|class|module|puts|attr_)/, + php: /(<\?php|\$[a-zA-Z_]|namespace|use\s+.*?;)/, + go: /(package main|import \(|func|type struct)/, + rust: /(fn main|let mut|impl|pub struct)/, + swift: /(import Foundation|var|func|class|struct)/, + kotlin: /(fun|val|var|class|package)/, + typescript: /(interface|type|export|implements)/ + }; + + for (const [lang, pattern] of Object.entries(languagePatterns)) { + if (pattern.test(code)) { + return lang; + } + } + + return 'javascript'; // Default to JavaScript if no match +} + +// Detect code smells in a code segment +function detectCodeSmells(code, fileExtension = '') { + const language = detectLanguage(code, fileExtension); + const rules = languageRules[language] || languageRules.javascript; + const smells = []; + + // Apply language-specific rules + rules.patterns.forEach(pattern => { + if (pattern.regex.test(code)) { + smells.push({ + type: 'smell', + message: pattern.message, + severity: 'warning' + }); + } + }); + + // Common patterns across languages + const commonSmells = [ + { regex: /TODO|FIXME/g, message: "Remove TODO/FIXME comments before committing" }, + { regex: /\/\/\s*hack/gi, message: "Remove hack comments" }, + { regex: /function.*\{[\s\S]{100,}\}/g, message: "Function is too long" }, + { regex: /(if|while).*\{[\s\S]*\1.*\{/g, message: "Nested control structures detected" }, + { regex: /[^\w\s\(\)]{3,}/g, message: "Complex expression detected" } + ]; + + commonSmells.forEach(pattern => { + if (pattern.regex.test(code)) { + smells.push({ + type: 'smell', + message: pattern.message, + severity: 'info' + }); + } + }); + + return smells; +} + /** * Calculate cognitive complexity of a code segment */ @@ -188,51 +363,25 @@ export function getComplexityColor(complexity) { } /** - * Detect code smells in a code segment + * Main analysis function for code segments */ -export function detectCodeSmells(code) { - const smells = []; - const lines = code.split('\n'); - - // Check each line for code smells - lines.forEach((line, lineNumber) => { - Object.entries(CODE_SMELL_PATTERNS).forEach(([type, { pattern, message, severity }]) => { - if (pattern.test(line)) { - smells.push({ - type, - message, - severity, - line: lineNumber + 1, - code: line.trim() - }); - } - }); - }); +export function analyzeCodeSegment(code, fileExtension = '') { + const complexity = calculateComplexity(code); + const dependencies = analyzeDependencies(code); + const sizeScore = calculateSizeScore(code); + const codeSmells = detectCodeSmells(code, fileExtension); + const performanceImpact = analyzePerformanceImpact(code); + const changeImpact = calculateChangeImpact(code); - // Check entire code block for multi-line patterns - Object.entries(CODE_SMELL_PATTERNS).forEach(([type, { pattern, message, severity }]) => { - if (pattern.test(code)) { - // Avoid duplicate reports for patterns that were already caught line by line - const alreadyReported = smells.some(smell => - smell.type === type && smell.code === code.match(pattern)?.[0]?.trim() - ); - - if (!alreadyReported) { - smells.push({ - type, - message, - severity, - code: code.match(pattern)?.[0]?.trim() || '' - }); - } - } - }); - - // Sort by severity (error > warning > info) - return smells.sort((a, b) => { - const severityOrder = { error: 0, warning: 1, info: 2 }; - return severityOrder[a.severity] - severityOrder[b.severity]; - }); + return { + complexity, + dependencies, + sizeScore, + codeSmells, + performanceImpact, + changeImpact, + color: getComplexityColor(complexity) + }; } /** @@ -264,25 +413,3 @@ export function calculateChangeImpact(code) { complexity > 4 || totalDependencies > 3 ? 'medium' : 'low' }; } - -/** - * Main analysis function for code segments - */ -export function analyzeCodeSegment(code) { - const complexity = calculateComplexity(code); - const dependencies = analyzeDependencies(code); - const sizeScore = calculateSizeScore(code); - const codeSmells = detectCodeSmells(code); - const performanceImpact = analyzePerformanceImpact(code); - const changeImpact = calculateChangeImpact(code); - - return { - complexity, - dependencies, - sizeScore, - codeSmells, - performanceImpact, - changeImpact, - color: getComplexityColor(complexity) - }; -} From 3fc4ff777ac3157dd390a715bddb109992eca7fd Mon Sep 17 00:00:00 2001 From: "ben.elyess@cgm.com" Date: Sun, 5 Jan 2025 16:29:35 +0100 Subject: [PATCH 06/14] fix block padding --- src/app/page.js | 96 ++++++++++++++++++------------------------------- 1 file changed, 35 insertions(+), 61 deletions(-) diff --git a/src/app/page.js b/src/app/page.js index 7a1e09b..acefd9d 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -387,6 +387,7 @@ const CodeTimeline = () => { ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200 shadow-sm" }`} + style={{ position: 'relative' }} >
{ style={{ transform: `scale(${zoom})`, transformOrigin: 'top left', - minHeight: '100%' + minHeight: '100%', + position: 'relative' }} > {filteredTimelineData.map((row) => { const analysis = analyzeCodeSegment(row.segments.map(s => s.text).join('')); return ( -
+
{ > {row.id} -
+
{row.segments.map((segment, segIndex) => (
{ - const tooltip = e.currentTarget.querySelector('.tooltip'); - if (tooltip) { - tooltip.style.display = 'block'; - } - }} - onMouseLeave={(e) => { - const tooltip = e.currentTarget.querySelector('.tooltip'); - if (tooltip) { - tooltip.style.display = 'none'; - } - }} + className="relative" >
{ width: `${segment.width}px`, cursor: 'pointer' }} + onMouseEnter={(e) => { + const tooltip = e.currentTarget.nextElementSibling; + if (tooltip) { + tooltip.style.display = 'block'; + } + }} + onMouseLeave={(e) => { + const tooltip = e.currentTarget.nextElementSibling; + if (tooltip) { + tooltip.style.display = 'none'; + } + }} />