diff options
| author | Taylor Mullen <[email protected]> | 2025-04-15 21:41:08 -0700 |
|---|---|---|
| committer | Taylor Mullen <[email protected]> | 2025-04-17 13:19:55 -0400 |
| commit | add233c5043264d47ecc6d3339a383f41a241ae8 (patch) | |
| tree | 3d80d412ed805007132cf44257bbd7667005dcd8 /packages/cli/src/ui/components/messages/DiffRenderer.tsx | |
Initial commit of Gemini Code CLI
This commit introduces the initial codebase for the Gemini Code CLI, a command-line interface designed to facilitate interaction with the Gemini API for software engineering tasks.
The code was migrated from a previous git repository as a single squashed commit.
Core Features & Components:
* **Gemini Integration:** Leverages the `@google/genai` SDK to interact with the Gemini models, supporting chat history, streaming responses, and function calling (tools).
* **Terminal UI:** Built with Ink (React for CLIs) providing an interactive chat interface within the terminal, including input prompts, message display, loading indicators, and tool interaction elements.
* **Tooling Framework:** Implements a robust tool system allowing Gemini to interact with the local environment. Includes tools for:
* File system listing (`ls`)
* File reading (`read-file`)
* Content searching (`grep`)
* File globbing (`glob`)
* File editing (`edit`)
* File writing (`write-file`)
* Executing bash commands (`terminal`)
* **State Management:** Handles the streaming state of Gemini responses and manages the conversation history.
* **Configuration:** Parses command-line arguments (`yargs`) and loads environment variables (`dotenv`) for setup.
* **Project Structure:** Organized into `core`, `ui`, `tools`, `config`, and `utils` directories using TypeScript. Includes basic build (`tsc`) and start scripts.
This initial version establishes the foundation for a powerful CLI tool enabling developers to use Gemini for coding assistance directly in their terminal environment.
---
Created by yours truly: __Gemini Code__
Diffstat (limited to 'packages/cli/src/ui/components/messages/DiffRenderer.tsx')
| -rw-r--r-- | packages/cli/src/ui/components/messages/DiffRenderer.tsx | 152 |
1 files changed, 152 insertions, 0 deletions
diff --git a/packages/cli/src/ui/components/messages/DiffRenderer.tsx b/packages/cli/src/ui/components/messages/DiffRenderer.tsx new file mode 100644 index 00000000..5cae9004 --- /dev/null +++ b/packages/cli/src/ui/components/messages/DiffRenderer.tsx @@ -0,0 +1,152 @@ +import React from 'react'; +import { Box, Text } from 'ink' + +interface DiffLine { + type: 'add' | 'del' | 'context' | 'hunk' | 'other'; + oldLine?: number; + newLine?: number; + content: string; +} + +function parseDiffWithLineNumbers(diffContent: string): DiffLine[] { + const lines = diffContent.split('\n'); + const result: DiffLine[] = []; + let currentOldLine = 0; + let currentNewLine = 0; + let inHunk = false; + const hunkHeaderRegex = /^@@ -(\d+),?\d* \+(\d+),?\d* @@/; + + for (const line of lines) { + const hunkMatch = line.match(hunkHeaderRegex); + if (hunkMatch) { + currentOldLine = parseInt(hunkMatch[1], 10); + currentNewLine = parseInt(hunkMatch[2], 10); + inHunk = true; + result.push({ type: 'hunk', content: line }); + // We need to adjust the starting point because the first line number applies to the *first* actual line change/context, + // but we increment *before* pushing that line. So decrement here. + currentOldLine--; + currentNewLine--; + continue; + } + if (!inHunk) { + // Skip standard Git header lines more robustly + if (line.startsWith('--- ') || line.startsWith('+++ ') || line.startsWith('diff --git') || line.startsWith('index ') || line.startsWith('similarity index') || line.startsWith('rename from') || line.startsWith('rename to') || line.startsWith('new file mode') || line.startsWith('deleted file mode')) continue; + // If it's not a hunk or header, skip (or handle as 'other' if needed) + continue; + } + if (line.startsWith('+')) { + currentNewLine++; // Increment before pushing + result.push({ type: 'add', newLine: currentNewLine, content: line.substring(1) }); + } else if (line.startsWith('-')) { + currentOldLine++; // Increment before pushing + result.push({ type: 'del', oldLine: currentOldLine, content: line.substring(1) }); + } else if (line.startsWith(' ')) { + currentOldLine++; // Increment before pushing + currentNewLine++; + result.push({ type: 'context', oldLine: currentOldLine, newLine: currentNewLine, content: line.substring(1) }); + } else if (line.startsWith('\\')) { // Handle "\ No newline at end of file" + result.push({ type: 'other', content: line }); + } + } + return result; +} + + +interface DiffRendererProps { + diffContent: string; + filename?: string; + tabWidth?: number; +} + +const DEFAULT_TAB_WIDTH = 4; // Spaces per tab for normalization + +const DiffRenderer: React.FC<DiffRendererProps> = ({ diffContent, tabWidth = DEFAULT_TAB_WIDTH }) => { + if (!diffContent || typeof diffContent !== 'string') { + return <Text color="yellow">No diff content.</Text>; + } + + const parsedLines = parseDiffWithLineNumbers(diffContent); + + // 1. Normalize whitespace (replace tabs with spaces) *before* further processing + const normalizedLines = parsedLines.map(line => ({ + ...line, + content: line.content.replace(/\t/g, ' '.repeat(tabWidth)) + })); + + // Filter out non-displayable lines (hunks, potentially 'other') using the normalized list + const displayableLines = normalizedLines.filter(l => l.type !== 'hunk' && l.type !== 'other'); + + + if (displayableLines.length === 0) { + return ( + <Box borderStyle="round" borderColor="gray" padding={1}> + <Text dimColor>No changes detected.</Text> + </Box> + ); + } + + // Calculate the minimum indentation across all displayable lines + let baseIndentation = Infinity; // Start high to find the minimum + for (const line of displayableLines) { + // Only consider lines with actual content for indentation calculation + if (line.content.trim() === '') continue; + + const firstCharIndex = line.content.search(/\S/); // Find index of first non-whitespace char + const currentIndent = (firstCharIndex === -1) ? 0 : firstCharIndex; // Indent is 0 if no non-whitespace found + baseIndentation = Math.min(baseIndentation, currentIndent); + } + // If baseIndentation remained Infinity (e.g., no displayable lines with content), default to 0 + if (!isFinite(baseIndentation)) { + baseIndentation = 0; + } + // --- End Modification --- + + + return ( + <Box borderStyle="round" borderColor="gray" flexDirection="column"> + {/* Iterate over the lines that should be displayed (already normalized) */} + {displayableLines.map((line, index) => { + const key = `diff-line-${index}`; + let gutterNumStr = ''; + let color: string | undefined = undefined; + let prefixSymbol = ' '; + let dim = false; + + switch (line.type) { + case 'add': + gutterNumStr = (line.newLine ?? '').toString(); + color = 'green'; + prefixSymbol = '+'; + break; + case 'del': + gutterNumStr = (line.oldLine ?? '').toString(); + color = 'red'; + prefixSymbol = '-'; + break; + case 'context': + // Show new line number for context lines in gutter + gutterNumStr = (line.newLine ?? '').toString(); + dim = true; + prefixSymbol = ' '; + break; + } + + // Render the line content *after* stripping the calculated *minimum* baseIndentation. + // The line.content here is already the tab-normalized version. + const displayContent = line.content.substring(baseIndentation); + + return ( + // Using your original rendering structure + <Box key={key} flexDirection="row"> + <Text color="gray">{gutterNumStr} </Text> + <Text color={color} dimColor={dim}>{prefixSymbol} </Text> + <Text color={color} dimColor={dim} wrap="wrap">{displayContent}</Text> + </Box> + ); + })} + </Box> + ); +}; + +export default DiffRenderer;
\ No newline at end of file |
