refactor: Enhance Diff View with advanced line and character-level change detection
- Improved diff algorithm to detect more granular line and character-level changes - Added support for character-level highlighting in diff view - Simplified diff view mode by removing side-by-side option - Updated component rendering to support more detailed change visualization - Optimized line change detection with improved matching strategy
This commit is contained in:
@@ -25,6 +25,10 @@ interface DiffBlock {
|
|||||||
content: string;
|
content: string;
|
||||||
type: 'added' | 'removed' | 'unchanged';
|
type: 'added' | 'removed' | 'unchanged';
|
||||||
correspondingLine?: number;
|
correspondingLine?: number;
|
||||||
|
charChanges?: Array<{
|
||||||
|
value: string;
|
||||||
|
type: 'added' | 'removed' | 'unchanged';
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FullscreenButtonProps {
|
interface FullscreenButtonProps {
|
||||||
@@ -74,93 +78,211 @@ const processChanges = (beforeCode: string, afterCode: string) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalizar quebras de linha para evitar falsos positivos
|
// Normalize line endings and content
|
||||||
const normalizedBefore = beforeCode.replace(/\r\n/g, '\n');
|
const normalizeContent = (content: string): string[] => {
|
||||||
const normalizedAfter = afterCode.replace(/\r\n/g, '\n');
|
return content
|
||||||
|
.replace(/\r\n/g, '\n')
|
||||||
|
.split('\n')
|
||||||
|
.map(line => line.trimEnd());
|
||||||
|
};
|
||||||
|
|
||||||
// Dividir em linhas preservando linhas vazias
|
const beforeLines = normalizeContent(beforeCode);
|
||||||
const beforeLines = normalizedBefore.split('\n');
|
const afterLines = normalizeContent(afterCode);
|
||||||
const afterLines = normalizedAfter.split('\n');
|
|
||||||
|
|
||||||
// Se os conteúdos são idênticos após normalização, não há mudanças
|
// Early return if files are identical
|
||||||
if (normalizedBefore === normalizedAfter) {
|
if (beforeLines.join('\n') === afterLines.join('\n')) {
|
||||||
return {
|
return {
|
||||||
beforeLines,
|
beforeLines,
|
||||||
afterLines,
|
afterLines,
|
||||||
hasChanges: false,
|
hasChanges: false,
|
||||||
lineChanges: { before: new Set(), after: new Set() },
|
lineChanges: { before: new Set(), after: new Set() },
|
||||||
unifiedBlocks: []
|
unifiedBlocks: [],
|
||||||
|
isBinary: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Processar as diferenças com configurações otimizadas para detecção por linha
|
|
||||||
const changes = diffLines(normalizedBefore, normalizedAfter, {
|
|
||||||
newlineIsToken: false, // Não tratar quebras de linha como tokens separados
|
|
||||||
ignoreWhitespace: true, // Ignorar diferenças de espaços em branco
|
|
||||||
ignoreCase: false // Manter sensibilidade a maiúsculas/minúsculas
|
|
||||||
});
|
|
||||||
|
|
||||||
const lineChanges = {
|
const lineChanges = {
|
||||||
before: new Set<number>(),
|
before: new Set<number>(),
|
||||||
after: new Set<number>()
|
after: new Set<number>()
|
||||||
};
|
};
|
||||||
|
|
||||||
let beforeLineNumber = 0;
|
const unifiedBlocks: DiffBlock[] = [];
|
||||||
let afterLineNumber = 0;
|
|
||||||
|
|
||||||
const unifiedBlocks = changes.reduce((blocks: DiffBlock[], change) => {
|
// Compare lines directly for more accurate diff
|
||||||
// Dividir o conteúdo em linhas preservando linhas vazias
|
let i = 0, j = 0;
|
||||||
const lines = change.value.split('\n');
|
while (i < beforeLines.length || j < afterLines.length) {
|
||||||
|
if (i < beforeLines.length && j < afterLines.length && beforeLines[i] === afterLines[j]) {
|
||||||
if (change.added) {
|
// Unchanged line
|
||||||
// Processar linhas adicionadas
|
unifiedBlocks.push({
|
||||||
const addedBlocks = lines.map((line, i) => {
|
lineNumber: j,
|
||||||
lineChanges.after.add(afterLineNumber + i);
|
content: afterLines[j],
|
||||||
return {
|
type: 'unchanged',
|
||||||
lineNumber: afterLineNumber + i,
|
correspondingLine: i
|
||||||
content: line,
|
|
||||||
type: 'added' as const
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
afterLineNumber += lines.length;
|
i++;
|
||||||
return [...blocks, ...addedBlocks];
|
j++;
|
||||||
|
} else {
|
||||||
|
// Look ahead for potential matches
|
||||||
|
let matchFound = false;
|
||||||
|
const lookAhead = 3; // Number of lines to look ahead
|
||||||
|
|
||||||
|
// Try to find matching lines ahead
|
||||||
|
for (let k = 1; k <= lookAhead && i + k < beforeLines.length && j + k < afterLines.length; k++) {
|
||||||
|
if (beforeLines[i + k] === afterLines[j]) {
|
||||||
|
// Found match in after lines - mark lines as removed
|
||||||
|
for (let l = 0; l < k; l++) {
|
||||||
|
lineChanges.before.add(i + l);
|
||||||
|
unifiedBlocks.push({
|
||||||
|
lineNumber: i + l,
|
||||||
|
content: beforeLines[i + l],
|
||||||
|
type: 'removed',
|
||||||
|
correspondingLine: j,
|
||||||
|
charChanges: [{ value: beforeLines[i + l], type: 'removed' }]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
i += k;
|
||||||
|
matchFound = true;
|
||||||
|
break;
|
||||||
|
} else if (beforeLines[i] === afterLines[j + k]) {
|
||||||
|
// Found match in before lines - mark lines as added
|
||||||
|
for (let l = 0; l < k; l++) {
|
||||||
|
lineChanges.after.add(j + l);
|
||||||
|
unifiedBlocks.push({
|
||||||
|
lineNumber: j + l,
|
||||||
|
content: afterLines[j + l],
|
||||||
|
type: 'added',
|
||||||
|
correspondingLine: i,
|
||||||
|
charChanges: [{ value: afterLines[j + l], type: 'added' }]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
j += k;
|
||||||
|
matchFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (change.removed) {
|
if (!matchFound) {
|
||||||
// Processar linhas removidas
|
// No match found - try to find character-level changes
|
||||||
const removedBlocks = lines.map((line, i) => {
|
if (i < beforeLines.length && j < afterLines.length) {
|
||||||
lineChanges.before.add(beforeLineNumber + i);
|
const beforeLine = beforeLines[i];
|
||||||
return {
|
const afterLine = afterLines[j];
|
||||||
lineNumber: beforeLineNumber + i,
|
|
||||||
content: line,
|
// Find common prefix and suffix
|
||||||
type: 'removed' as const
|
let prefixLength = 0;
|
||||||
};
|
while (prefixLength < beforeLine.length &&
|
||||||
});
|
prefixLength < afterLine.length &&
|
||||||
beforeLineNumber += lines.length;
|
beforeLine[prefixLength] === afterLine[prefixLength]) {
|
||||||
return [...blocks, ...removedBlocks];
|
prefixLength++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Processar linhas não modificadas
|
let suffixLength = 0;
|
||||||
const unchangedBlocks = lines.map((line, i) => {
|
while (suffixLength < beforeLine.length - prefixLength &&
|
||||||
const block = {
|
suffixLength < afterLine.length - prefixLength &&
|
||||||
lineNumber: afterLineNumber + i,
|
beforeLine[beforeLine.length - 1 - suffixLength] ===
|
||||||
content: line,
|
afterLine[afterLine.length - 1 - suffixLength]) {
|
||||||
type: 'unchanged' as const,
|
suffixLength++;
|
||||||
correspondingLine: beforeLineNumber + i
|
}
|
||||||
};
|
|
||||||
return block;
|
const prefix = beforeLine.slice(0, prefixLength);
|
||||||
|
const beforeMiddle = beforeLine.slice(prefixLength, beforeLine.length - suffixLength);
|
||||||
|
const afterMiddle = afterLine.slice(prefixLength, afterLine.length - suffixLength);
|
||||||
|
const suffix = beforeLine.slice(beforeLine.length - suffixLength);
|
||||||
|
|
||||||
|
if (beforeMiddle || afterMiddle) {
|
||||||
|
// There are character-level changes
|
||||||
|
if (beforeMiddle) {
|
||||||
|
lineChanges.before.add(i);
|
||||||
|
unifiedBlocks.push({
|
||||||
|
lineNumber: i,
|
||||||
|
content: beforeLine,
|
||||||
|
type: 'removed',
|
||||||
|
correspondingLine: j,
|
||||||
|
charChanges: [
|
||||||
|
{ value: prefix, type: 'unchanged' },
|
||||||
|
{ value: beforeMiddle, type: 'removed' },
|
||||||
|
{ value: suffix, type: 'unchanged' }
|
||||||
|
]
|
||||||
});
|
});
|
||||||
beforeLineNumber += lines.length;
|
i++;
|
||||||
afterLineNumber += lines.length;
|
}
|
||||||
return [...blocks, ...unchangedBlocks];
|
if (afterMiddle) {
|
||||||
}, []);
|
lineChanges.after.add(j);
|
||||||
|
unifiedBlocks.push({
|
||||||
|
lineNumber: j,
|
||||||
|
content: afterLine,
|
||||||
|
type: 'added',
|
||||||
|
correspondingLine: i - 1,
|
||||||
|
charChanges: [
|
||||||
|
{ value: prefix, type: 'unchanged' },
|
||||||
|
{ value: afterMiddle, type: 'added' },
|
||||||
|
{ value: suffix, type: 'unchanged' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No character-level changes found, treat as regular line changes
|
||||||
|
if (i < beforeLines.length) {
|
||||||
|
lineChanges.before.add(i);
|
||||||
|
unifiedBlocks.push({
|
||||||
|
lineNumber: i,
|
||||||
|
content: beforeLines[i],
|
||||||
|
type: 'removed',
|
||||||
|
correspondingLine: j,
|
||||||
|
charChanges: [{ value: beforeLines[i], type: 'removed' }]
|
||||||
|
});
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
if (j < afterLines.length) {
|
||||||
|
lineChanges.after.add(j);
|
||||||
|
unifiedBlocks.push({
|
||||||
|
lineNumber: j,
|
||||||
|
content: afterLines[j],
|
||||||
|
type: 'added',
|
||||||
|
correspondingLine: i - 1,
|
||||||
|
charChanges: [{ value: afterLines[j], type: 'added' }]
|
||||||
|
});
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle remaining lines
|
||||||
|
if (i < beforeLines.length) {
|
||||||
|
lineChanges.before.add(i);
|
||||||
|
unifiedBlocks.push({
|
||||||
|
lineNumber: i,
|
||||||
|
content: beforeLines[i],
|
||||||
|
type: 'removed',
|
||||||
|
correspondingLine: j,
|
||||||
|
charChanges: [{ value: beforeLines[i], type: 'removed' }]
|
||||||
|
});
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
if (j < afterLines.length) {
|
||||||
|
lineChanges.after.add(j);
|
||||||
|
unifiedBlocks.push({
|
||||||
|
lineNumber: j,
|
||||||
|
content: afterLines[j],
|
||||||
|
type: 'added',
|
||||||
|
correspondingLine: i - 1,
|
||||||
|
charChanges: [{ value: afterLines[j], type: 'added' }]
|
||||||
|
});
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort blocks by line number
|
||||||
|
const processedBlocks = unifiedBlocks.sort((a, b) => a.lineNumber - b.lineNumber);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
beforeLines,
|
beforeLines,
|
||||||
afterLines,
|
afterLines,
|
||||||
hasChanges: lineChanges.before.size > 0 || lineChanges.after.size > 0,
|
hasChanges: lineChanges.before.size > 0 || lineChanges.after.size > 0,
|
||||||
lineChanges,
|
lineChanges,
|
||||||
unifiedBlocks,
|
unifiedBlocks: processedBlocks,
|
||||||
isBinary: false
|
isBinary: false
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -177,8 +299,14 @@ const processChanges = (beforeCode: string, afterCode: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const lineNumberStyles = "w-12 shrink-0 pl-2 py-0.5 text-left font-mono text-bolt-elements-textTertiary border-r border-bolt-elements-borderColor bg-bolt-elements-background-depth-1";
|
const lineNumberStyles = "w-9 shrink-0 pl-2 py-1 text-left font-mono text-bolt-elements-textTertiary border-r border-bolt-elements-borderColor bg-bolt-elements-background-depth-1";
|
||||||
const lineContentStyles = "px-4 py-0.5 font-mono whitespace-pre flex-1 group-hover:bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary";
|
const lineContentStyles = "px-1 py-1 font-mono whitespace-pre flex-1 group-hover:bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary";
|
||||||
|
const diffPanelStyles = "h-full overflow-auto diff-panel-content";
|
||||||
|
const diffLineStyles = {
|
||||||
|
added: 'bg-green-500/20 border-l-4 border-green-500',
|
||||||
|
removed: 'bg-red-500/20 border-l-4 border-red-500',
|
||||||
|
unchanged: ''
|
||||||
|
};
|
||||||
|
|
||||||
const renderContentWarning = (type: 'binary' | 'error') => (
|
const renderContentWarning = (type: 'binary' | 'error') => (
|
||||||
<div className="h-full flex items-center justify-center p-4">
|
<div className="h-full flex items-center justify-center p-4">
|
||||||
@@ -243,13 +371,15 @@ const CodeLine = memo(({
|
|||||||
content,
|
content,
|
||||||
type,
|
type,
|
||||||
highlighter,
|
highlighter,
|
||||||
language
|
language,
|
||||||
|
block
|
||||||
}: {
|
}: {
|
||||||
lineNumber: number;
|
lineNumber: number;
|
||||||
content: string;
|
content: string;
|
||||||
type: 'added' | 'removed' | 'unchanged';
|
type: 'added' | 'removed' | 'unchanged';
|
||||||
highlighter: any;
|
highlighter: any;
|
||||||
language: string;
|
language: string;
|
||||||
|
block: DiffBlock;
|
||||||
}) => {
|
}) => {
|
||||||
const bgColor = {
|
const bgColor = {
|
||||||
added: 'bg-green-500/20 border-l-4 border-green-500',
|
added: 'bg-green-500/20 border-l-4 border-green-500',
|
||||||
@@ -257,13 +387,42 @@ const CodeLine = memo(({
|
|||||||
unchanged: ''
|
unchanged: ''
|
||||||
}[type];
|
}[type];
|
||||||
|
|
||||||
const highlightedCode = useMemo(() => {
|
const renderContent = () => {
|
||||||
if (!highlighter) return content;
|
if (type === 'unchanged' || !block.charChanges) {
|
||||||
return highlighter.codeToHtml(content, {
|
const highlightedCode = highlighter ?
|
||||||
lang: language,
|
highlighter.codeToHtml(content, { lang: language, theme: 'github-dark' })
|
||||||
theme: 'github-dark'
|
.replace(/<\/?pre[^>]*>/g, '')
|
||||||
}).replace(/<\/?pre[^>]*>/g, '').replace(/<\/?code[^>]*>/g, '');
|
.replace(/<\/?code[^>]*>/g, '')
|
||||||
}, [content, highlighter, language]);
|
: content;
|
||||||
|
return <span dangerouslySetInnerHTML={{ __html: highlightedCode }} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{block.charChanges.map((change, index) => {
|
||||||
|
const changeClass = {
|
||||||
|
added: 'text-green-500 bg-green-500/20',
|
||||||
|
removed: 'text-red-500 bg-red-500/20',
|
||||||
|
unchanged: ''
|
||||||
|
}[change.type];
|
||||||
|
|
||||||
|
const highlightedCode = highlighter ?
|
||||||
|
highlighter.codeToHtml(change.value, { lang: language, theme: 'github-dark' })
|
||||||
|
.replace(/<\/?pre[^>]*>/g, '')
|
||||||
|
.replace(/<\/?code[^>]*>/g, '')
|
||||||
|
: change.value;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className={changeClass}
|
||||||
|
dangerouslySetInnerHTML={{ __html: highlightedCode }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex group min-w-fit">
|
<div className="flex group min-w-fit">
|
||||||
@@ -274,7 +433,7 @@ const CodeLine = memo(({
|
|||||||
{type === 'removed' && '-'}
|
{type === 'removed' && '-'}
|
||||||
{type === 'unchanged' && ' '}
|
{type === 'unchanged' && ' '}
|
||||||
</span>
|
</span>
|
||||||
<span dangerouslySetInnerHTML={{ __html: highlightedCode }} />
|
{renderContent()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -380,9 +539,9 @@ const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language,
|
|||||||
beforeCode={beforeCode}
|
beforeCode={beforeCode}
|
||||||
afterCode={afterCode}
|
afterCode={afterCode}
|
||||||
/>
|
/>
|
||||||
<div className="flex-1 overflow-auto diff-panel-content">
|
<div className={diffPanelStyles}>
|
||||||
{hasChanges ? (
|
{hasChanges ? (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto min-w-full">
|
||||||
{unifiedBlocks.map((block, index) => (
|
{unifiedBlocks.map((block, index) => (
|
||||||
<CodeLine
|
<CodeLine
|
||||||
key={`${block.lineNumber}-${index}`}
|
key={`${block.lineNumber}-${index}`}
|
||||||
@@ -391,6 +550,7 @@ const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language,
|
|||||||
type={block.type}
|
type={block.type}
|
||||||
highlighter={highlighter}
|
highlighter={highlighter}
|
||||||
language={language}
|
language={language}
|
||||||
|
block={block}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -407,103 +567,13 @@ const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language,
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const SideBySideComparison = memo(({
|
|
||||||
beforeCode,
|
|
||||||
afterCode,
|
|
||||||
language,
|
|
||||||
filename,
|
|
||||||
lightTheme,
|
|
||||||
darkTheme,
|
|
||||||
}: CodeComparisonProps) => {
|
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
||||||
const [highlighter, setHighlighter] = useState<any>(null);
|
|
||||||
|
|
||||||
const toggleFullscreen = useCallback(() => {
|
|
||||||
setIsFullscreen(prev => !prev);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const { beforeLines, afterLines, hasChanges, lineChanges, isBinary, error } = useProcessChanges(beforeCode, afterCode);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getHighlighter({
|
|
||||||
themes: ['github-dark'],
|
|
||||||
langs: ['typescript', 'javascript', 'json', 'html', 'css', 'jsx', 'tsx']
|
|
||||||
}).then(setHighlighter);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (isBinary || error) return renderContentWarning(isBinary ? 'binary' : 'error');
|
|
||||||
|
|
||||||
const renderCode = (code: string) => {
|
|
||||||
if (!highlighter) return code;
|
|
||||||
const highlightedCode = highlighter.codeToHtml(code, {
|
|
||||||
lang: language,
|
|
||||||
theme: 'github-dark'
|
|
||||||
});
|
|
||||||
return highlightedCode.replace(/<\/?pre[^>]*>/g, '').replace(/<\/?code[^>]*>/g, '');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FullscreenOverlay isFullscreen={isFullscreen}>
|
|
||||||
<div className="w-full h-full flex flex-col">
|
|
||||||
<FileInfo
|
|
||||||
filename={filename}
|
|
||||||
hasChanges={hasChanges}
|
|
||||||
onToggleFullscreen={toggleFullscreen}
|
|
||||||
isFullscreen={isFullscreen}
|
|
||||||
beforeCode={beforeCode}
|
|
||||||
afterCode={afterCode}
|
|
||||||
/>
|
|
||||||
<div className="flex-1 overflow-auto diff-panel-content">
|
|
||||||
{hasChanges ? (
|
|
||||||
<div className="grid md:grid-cols-2 divide-x divide-bolt-elements-borderColor relative h-full">
|
|
||||||
<div className="overflow-auto">
|
|
||||||
{beforeLines.map((line, index) => (
|
|
||||||
<div key={`before-${index}`} className="flex group min-w-fit">
|
|
||||||
<div className={lineNumberStyles}>{index + 1}</div>
|
|
||||||
<div className={`${lineContentStyles} ${lineChanges.before.has(index) ? 'bg-red-500/20 border-l-4 border-red-500' : ''}`}>
|
|
||||||
<span className="mr-2 text-bolt-elements-textTertiary">
|
|
||||||
{lineChanges.before.has(index) ? '-' : ' '}
|
|
||||||
</span>
|
|
||||||
<span dangerouslySetInnerHTML={{ __html: renderCode(line) }} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="overflow-auto">
|
|
||||||
{afterLines.map((line, index) => (
|
|
||||||
<div key={`after-${index}`} className="flex group min-w-fit">
|
|
||||||
<div className={lineNumberStyles}>{index + 1}</div>
|
|
||||||
<div className={`${lineContentStyles} ${lineChanges.after.has(index) ? 'bg-green-500/20 border-l-4 border-green-500' : ''}`}>
|
|
||||||
<span className="mr-2 text-bolt-elements-textTertiary">
|
|
||||||
{lineChanges.after.has(index) ? '+' : ' '}
|
|
||||||
</span>
|
|
||||||
<span dangerouslySetInnerHTML={{ __html: renderCode(line) }} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<NoChangesView
|
|
||||||
beforeCode={beforeCode}
|
|
||||||
language={language}
|
|
||||||
highlighter={highlighter}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</FullscreenOverlay>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
interface DiffViewProps {
|
interface DiffViewProps {
|
||||||
fileHistory: Record<string, FileHistory>;
|
fileHistory: Record<string, FileHistory>;
|
||||||
setFileHistory: React.Dispatch<React.SetStateAction<Record<string, FileHistory>>>;
|
setFileHistory: React.Dispatch<React.SetStateAction<Record<string, FileHistory>>>;
|
||||||
diffViewMode: 'inline' | 'side';
|
|
||||||
actionRunner: ActionRunner;
|
actionRunner: ActionRunner;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DiffView = memo(({ fileHistory, setFileHistory, diffViewMode, actionRunner }: DiffViewProps) => {
|
export const DiffView = memo(({ fileHistory, setFileHistory, actionRunner }: DiffViewProps) => {
|
||||||
const files = useStore(workbenchStore.files) as FileMap;
|
const files = useStore(workbenchStore.files) as FileMap;
|
||||||
const selectedFile = useStore(workbenchStore.selectedFile);
|
const selectedFile = useStore(workbenchStore.selectedFile);
|
||||||
const currentDocument = useStore(workbenchStore.currentDocument) as EditorDocument;
|
const currentDocument = useStore(workbenchStore.currentDocument) as EditorDocument;
|
||||||
@@ -612,7 +682,6 @@ export const DiffView = memo(({ fileHistory, setFileHistory, diffViewMode, actio
|
|||||||
try {
|
try {
|
||||||
return (
|
return (
|
||||||
<div className="h-full overflow-hidden">
|
<div className="h-full overflow-hidden">
|
||||||
{diffViewMode === 'inline' ? (
|
|
||||||
<InlineDiffComparison
|
<InlineDiffComparison
|
||||||
beforeCode={effectiveOriginalContent}
|
beforeCode={effectiveOriginalContent}
|
||||||
afterCode={currentContent}
|
afterCode={currentContent}
|
||||||
@@ -621,16 +690,6 @@ export const DiffView = memo(({ fileHistory, setFileHistory, diffViewMode, actio
|
|||||||
lightTheme="github-light"
|
lightTheme="github-light"
|
||||||
darkTheme="github-dark"
|
darkTheme="github-dark"
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<SideBySideComparison
|
|
||||||
beforeCode={effectiveOriginalContent}
|
|
||||||
afterCode={currentContent}
|
|
||||||
language={language}
|
|
||||||
filename={selectedFile}
|
|
||||||
lightTheme="github-light"
|
|
||||||
darkTheme="github-dark"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -74,13 +74,9 @@ const workbenchVariants = {
|
|||||||
const FileModifiedDropdown = memo(({
|
const FileModifiedDropdown = memo(({
|
||||||
fileHistory,
|
fileHistory,
|
||||||
onSelectFile,
|
onSelectFile,
|
||||||
diffViewMode,
|
|
||||||
toggleDiffViewMode,
|
|
||||||
}: {
|
}: {
|
||||||
fileHistory: Record<string, FileHistory>,
|
fileHistory: Record<string, FileHistory>,
|
||||||
onSelectFile: (filePath: string) => void,
|
onSelectFile: (filePath: string) => void,
|
||||||
diffViewMode: 'inline' | 'side',
|
|
||||||
toggleDiffViewMode: () => void,
|
|
||||||
}) => {
|
}) => {
|
||||||
const modifiedFiles = Object.entries(fileHistory);
|
const modifiedFiles = Object.entries(fileHistory);
|
||||||
const hasChanges = modifiedFiles.length > 0;
|
const hasChanges = modifiedFiles.length > 0;
|
||||||
@@ -251,12 +247,6 @@ const FileModifiedDropdown = memo(({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Popover>
|
</Popover>
|
||||||
<button
|
|
||||||
onClick={(e) => { e.stopPropagation(); toggleDiffViewMode(); }}
|
|
||||||
className="flex items-center gap-2 px-3 py-1.5 text-sm rounded-lg bg-bolt-elements-background-depth-2 hover:bg-bolt-elements-background-depth-3 transition-colors text-bolt-elements-textPrimary border border-bolt-elements-borderColor"
|
|
||||||
>
|
|
||||||
<span className="font-medium">{diffViewMode === 'inline' ? 'Inline' : 'Side by Side'}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -272,7 +262,6 @@ export const Workbench = memo(({
|
|||||||
|
|
||||||
const [isSyncing, setIsSyncing] = useState(false);
|
const [isSyncing, setIsSyncing] = useState(false);
|
||||||
const [isPushDialogOpen, setIsPushDialogOpen] = useState(false);
|
const [isPushDialogOpen, setIsPushDialogOpen] = useState(false);
|
||||||
const [diffViewMode, setDiffViewMode] = useState<'inline' | 'side'>('inline');
|
|
||||||
const [fileHistory, setFileHistory] = useState<Record<string, FileHistory>>({});
|
const [fileHistory, setFileHistory] = useState<Record<string, FileHistory>>({});
|
||||||
|
|
||||||
const modifiedFiles = Array.from(useStore(workbenchStore.unsavedFiles).keys());
|
const modifiedFiles = Array.from(useStore(workbenchStore.unsavedFiles).keys());
|
||||||
@@ -343,10 +332,6 @@ export const Workbench = memo(({
|
|||||||
workbenchStore.currentView.set('diff');
|
workbenchStore.currentView.set('diff');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const toggleDiffViewMode = useCallback(() => {
|
|
||||||
setDiffViewMode(prev => prev === 'inline' ? 'side' : 'inline');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
chatStarted && (
|
chatStarted && (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -405,8 +390,6 @@ export const Workbench = memo(({
|
|||||||
<FileModifiedDropdown
|
<FileModifiedDropdown
|
||||||
fileHistory={fileHistory}
|
fileHistory={fileHistory}
|
||||||
onSelectFile={handleSelectFile}
|
onSelectFile={handleSelectFile}
|
||||||
diffViewMode={diffViewMode}
|
|
||||||
toggleDiffViewMode={toggleDiffViewMode}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -444,7 +427,6 @@ export const Workbench = memo(({
|
|||||||
<DiffView
|
<DiffView
|
||||||
fileHistory={fileHistory}
|
fileHistory={fileHistory}
|
||||||
setFileHistory={setFileHistory}
|
setFileHistory={setFileHistory}
|
||||||
diffViewMode={diffViewMode}
|
|
||||||
actionRunner={actionRunner}
|
actionRunner={actionRunner}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -74,12 +74,11 @@
|
|||||||
"@radix-ui/react-switch": "^1.1.1",
|
"@radix-ui/react-switch": "^1.1.1",
|
||||||
"@radix-ui/react-tabs": "^1.1.2",
|
"@radix-ui/react-tabs": "^1.1.2",
|
||||||
"@radix-ui/react-tooltip": "^1.1.4",
|
"@radix-ui/react-tooltip": "^1.1.4",
|
||||||
"lucide-react": "^0.474.0",
|
|
||||||
"next-themes": "^0.4.4",
|
|
||||||
"@remix-run/cloudflare": "^2.15.2",
|
"@remix-run/cloudflare": "^2.15.2",
|
||||||
"@remix-run/cloudflare-pages": "^2.15.2",
|
"@remix-run/cloudflare-pages": "^2.15.2",
|
||||||
"@remix-run/node": "^2.15.2",
|
"@remix-run/node": "^2.15.2",
|
||||||
"@remix-run/react": "^2.15.2",
|
"@remix-run/react": "^2.15.2",
|
||||||
|
"@tanstack/react-virtual": "^3.13.0",
|
||||||
"@types/react-beautiful-dnd": "^13.1.8",
|
"@types/react-beautiful-dnd": "^13.1.8",
|
||||||
"@uiw/codemirror-theme-vscode": "^4.23.6",
|
"@uiw/codemirror-theme-vscode": "^4.23.6",
|
||||||
"@unocss/reset": "^0.61.9",
|
"@unocss/reset": "^0.61.9",
|
||||||
@@ -105,7 +104,9 @@
|
|||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"jspdf": "^2.5.2",
|
"jspdf": "^2.5.2",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
|
"lucide-react": "^0.474.0",
|
||||||
"nanostores": "^0.10.3",
|
"nanostores": "^0.10.3",
|
||||||
|
"next-themes": "^0.4.4",
|
||||||
"ollama-ai-provider": "^0.15.2",
|
"ollama-ai-provider": "^0.15.2",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
@@ -135,6 +136,8 @@
|
|||||||
"@iconify-json/ph": "^1.2.1",
|
"@iconify-json/ph": "^1.2.1",
|
||||||
"@iconify/types": "^2.0.0",
|
"@iconify/types": "^2.0.0",
|
||||||
"@remix-run/dev": "^2.15.2",
|
"@remix-run/dev": "^2.15.2",
|
||||||
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
|
"@testing-library/react": "^16.2.0",
|
||||||
"@types/diff": "^5.2.3",
|
"@types/diff": "^5.2.3",
|
||||||
"@types/dom-speech-recognition": "^0.0.4",
|
"@types/dom-speech-recognition": "^0.0.4",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
@@ -142,9 +145,11 @@
|
|||||||
"@types/path-browserify": "^1.0.3",
|
"@types/path-browserify": "^1.0.3",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
"husky": "9.1.7",
|
"husky": "9.1.7",
|
||||||
"is-ci": "^3.0.1",
|
"is-ci": "^3.0.1",
|
||||||
|
"jsdom": "^26.0.0",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"pnpm": "^9.14.4",
|
"pnpm": "^9.14.4",
|
||||||
"prettier": "^3.4.1",
|
"prettier": "^3.4.1",
|
||||||
|
|||||||
1250
pnpm-lock.yaml
generated
1250
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user