giga-code
Version:
A personal AI CLI assistant powered by Grok for local development.
241 lines • 11.6 kB
JavaScript
;
/**
* Professional diff renderer component
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DiffRenderer = void 0;
const react_1 = __importStar(require("react"));
const ink_1 = require("ink");
const colors_1 = require("../utils/colors");
const crypto_1 = __importDefault(require("crypto"));
const max_sized_box_1 = require("../shared/max-sized-box");
// Parsing function for expensive diff computation
function parseDiffWithLineNumbers(diffContent) {
const lines = diffContent.split('\n');
const result = [];
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;
}
const DEFAULT_TAB_WIDTH = 4; // Spaces per tab for normalization
// Memoized DiffRenderer component with shallow prop comparison
exports.DiffRenderer = react_1.default.memo(({ diffContent, filename, tabWidth = DEFAULT_TAB_WIDTH, availableTerminalHeight, terminalWidth = 80, }) => {
if (!diffContent || typeof diffContent !== 'string') {
return react_1.default.createElement(ink_1.Text, { color: colors_1.Colors.AccentYellow }, "No diff content.");
}
// Strip the first summary line (e.g. "Updated file.txt with 1 addition and 2 removals")
const lines = diffContent.split('\n');
const firstLine = lines[0];
let actualDiffContent = diffContent;
if (firstLine && (firstLine.startsWith('Updated ') || firstLine.startsWith('Created '))) {
actualDiffContent = lines.slice(1).join('\n');
}
// Use memoized parsing with content hash for caching
const contentHash = (0, react_1.useMemo)(() => crypto_1.default.createHash('sha1').update(actualDiffContent).digest('hex'), [actualDiffContent]);
const parsedLines = (0, react_1.useMemo)(() => parseDiffWithLineNumbers(actualDiffContent), [actualDiffContent]);
if (parsedLines.length === 0) {
return react_1.default.createElement(ink_1.Text, { dimColor: true }, "No changes detected.");
}
// Memoize expensive render computation
const renderedOutput = (0, react_1.useMemo)(() => renderDiffContent(parsedLines, filename, tabWidth, availableTerminalHeight, terminalWidth), [parsedLines, filename, tabWidth, availableTerminalHeight, terminalWidth]);
return react_1.default.createElement(react_1.default.Fragment, null, renderedOutput);
});
// Add display name for better debugging
exports.DiffRenderer.displayName = 'DiffRenderer';
const renderDiffContent = (parsedLines, filename, tabWidth = DEFAULT_TAB_WIDTH, availableTerminalHeight, terminalWidth) => {
// 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 react_1.default.createElement(ink_1.Text, { dimColor: true }, "No changes detected.");
}
// 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;
}
const key = filename
? `diff-box-${filename}-${crypto_1.default.createHash('sha1').update(JSON.stringify(parsedLines)).digest('hex')}`
: `diff-box-${crypto_1.default.createHash('sha1').update(JSON.stringify(parsedLines)).digest('hex')}`;
let lastLineNumber = null;
const MAX_CONTEXT_LINES_WITHOUT_GAP = 5;
return (react_1.default.createElement(max_sized_box_1.MaxSizedBox, { maxHeight: availableTerminalHeight, maxWidth: terminalWidth, key: key }, displayableLines.reduce((acc, line, index) => {
// Determine the relevant line number for gap calculation based on type
let relevantLineNumberForGapCalc = null;
if (line.type === 'add' || line.type === 'context') {
relevantLineNumberForGapCalc = line.newLine ?? null;
}
else if (line.type === 'del') {
// For deletions, the gap is typically in relation to the original file's line numbering
relevantLineNumberForGapCalc = line.oldLine ?? null;
}
if (lastLineNumber !== null &&
relevantLineNumberForGapCalc !== null &&
relevantLineNumberForGapCalc >
lastLineNumber + MAX_CONTEXT_LINES_WITHOUT_GAP + 1) {
acc.push(react_1.default.createElement(ink_1.Box, { key: `gap-${index}` },
react_1.default.createElement(ink_1.Text, { wrap: "truncate" }, '═'.repeat(terminalWidth))));
}
const lineKey = `diff-line-${index}`;
let gutterNumStr = '';
let backgroundColor = undefined;
let prefixSymbol = ' ';
let dim = false;
switch (line.type) {
case 'add':
gutterNumStr = (line.newLine ?? '').toString();
backgroundColor = '#86efac'; // Light green for additions
prefixSymbol = '+';
lastLineNumber = line.newLine ?? null;
break;
case 'del':
gutterNumStr = (line.oldLine ?? '').toString();
backgroundColor = 'redBright'; // Light red for deletions
prefixSymbol = '-';
// For deletions, update lastLineNumber based on oldLine if it's advancing.
// This helps manage gaps correctly if there are multiple consecutive deletions
// or if a deletion is followed by a context line far away in the original file.
if (line.oldLine !== undefined) {
lastLineNumber = line.oldLine;
}
break;
case 'context':
gutterNumStr = (line.newLine ?? '').toString();
dim = true;
prefixSymbol = ' ';
lastLineNumber = line.newLine ?? null;
break;
default:
return acc;
}
const displayContent = line.content.substring(baseIndentation);
acc.push(react_1.default.createElement(ink_1.Box, { key: lineKey, flexDirection: "row" },
react_1.default.createElement(ink_1.Text, { color: colors_1.Colors.Gray, dimColor: dim }, gutterNumStr.padEnd(4)),
react_1.default.createElement(ink_1.Text, { color: backgroundColor ? '#000000' : undefined, backgroundColor: backgroundColor, dimColor: !backgroundColor && dim },
prefixSymbol,
" "),
react_1.default.createElement(ink_1.Text, { color: backgroundColor ? '#000000' : undefined, backgroundColor: backgroundColor, dimColor: !backgroundColor && dim, wrap: "wrap" }, displayContent)));
return acc;
}, [])));
};
const getLanguageFromExtension = (extension) => {
const languageMap = {
js: 'javascript',
ts: 'typescript',
py: 'python',
json: 'json',
css: 'css',
html: 'html',
sh: 'bash',
md: 'markdown',
yaml: 'yaml',
yml: 'yaml',
txt: 'plaintext',
java: 'java',
c: 'c',
cpp: 'cpp',
rb: 'ruby',
};
return languageMap[extension] || null; // Return null if extension not found
};
//# sourceMappingURL=diff-renderer.js.map