apply diff view add editable view

This commit is contained in:
duanfuxiang
2025-04-22 18:03:51 +08:00
parent fe02f08bdf
commit d521184945
4 changed files with 450 additions and 75 deletions

View File

@@ -1,41 +1,59 @@
import { Change, diffLines } from 'diff'
import { CheckIcon, X } from 'lucide-react'
import { Platform, getIcon } from 'obsidian'
import { useEffect, useState } from 'react'
import ContentEditable from 'react-contenteditable'
import { ApplyViewState } from '../../ApplyView'
import { useApp } from '../../contexts/AppContext'
export default function ApplyViewRoot({
state,
close,
state,
close,
}: {
state: ApplyViewState
close: () => void
state: ApplyViewState
close: () => void
}) {
const acceptIcon = getIcon('check')
const rejectIcon = getIcon('x')
const excludeIcon = getIcon('x')
const acceptIcon = getIcon('check')
const rejectIcon = getIcon('x')
const excludeIcon = getIcon('x')
const getShortcutText = (shortcut: 'accept' | 'reject') => {
const isMac = Platform.isMacOS
if (shortcut === 'accept') {
return isMac ? '(⌘⏎)' : '(Ctrl+⏎)'
}
return isMac ? '(⌘⌫)' : '(Ctrl+⌫)'
}
const getShortcutText = (shortcut: 'accept' | 'reject') => {
const isMac = Platform.isMacOS
if (shortcut === 'accept') {
return isMac ? '(⌘⏎)' : '(Ctrl+⏎)'
}
return isMac ? '(⌘⌫)' : '(Ctrl+⌫)'
}
const app = useApp()
const app = useApp()
const [diff, setDiff] = useState<Change[]>(
diffLines(state.oldContent, state.newContent),
)
// Track which lines have been accepted or excluded
const [diffStatus, setDiffStatus] = useState<Array<'active' | 'accepted' | 'excluded'>>([])
const [diff] = useState<Change[]>(() => {
const initialDiff = diffLines(state.oldContent, state.newContent)
// Initialize all lines as 'active'
setDiffStatus(initialDiff.map(() => 'active'))
return initialDiff
})
// Store edited content for each diff part
const [editedContents, setEditedContents] = useState<string[]>(
diff.map(part => part.value)
)
const handleAccept = async () => {
const newContent = diff
.filter((change) => !change.removed)
.map((change) => change.value)
.join('')
const handleAccept = async () => {
// Filter and process content based on diffStatus
const newContent = diff.reduce((result, change, index) => {
// Keep unchanged content, non-excluded additions, or excluded removals
if ((!change.added && !change.removed) ||
(change.added && diffStatus[index] !== 'excluded') ||
(change.removed && diffStatus[index] === 'excluded')) {
return result + editedContents[index];
}
return result;
}, '')
await app.vault.modify(state.file, newContent)
if (state.onClose) {
state.onClose(true)
@@ -51,30 +69,20 @@ export default function ApplyViewRoot({
}
const excludeDiffLine = (index: number) => {
setDiff((prevDiff) => {
const newDiff = [...prevDiff]
const change = newDiff[index]
if (change.added) {
// Remove the entry if it's an added line
return newDiff.filter((_, i) => i !== index)
} else if (change.removed) {
change.removed = false
}
return newDiff
setDiffStatus(prevStatus => {
const newStatus = [...prevStatus]
// Mark line as excluded
newStatus[index] = 'excluded'
return newStatus
})
}
const acceptDiffLine = (index: number) => {
setDiff((prevDiff) => {
const newDiff = [...prevDiff]
const change = newDiff[index]
if (change.added) {
change.added = false
} else if (change.removed) {
// Remove the entry if it's a removed line
return newDiff.filter((_, i) => i !== index)
}
return newDiff
setDiffStatus(prevStatus => {
const newStatus = [...prevStatus]
// Mark line as accepted
newStatus[index] = 'accepted'
return newStatus
})
}
@@ -92,15 +100,22 @@ export default function ApplyViewRoot({
}
}
}
// Handle content editing changes
const handleContentChange = (index: number, evt: { target: { value: string } }) => {
const newEditedContents = [...editedContents];
newEditedContents[index] = evt.target.value;
setEditedContents(newEditedContents);
}
// 在组件挂载时添加事件监听器,在卸载时移除
// Add event listeners on mount and remove on unmount
useEffect(() => {
const handler = (e: KeyboardEvent) => handleKeyDown(e);
window.addEventListener('keydown', handler, true);
return () => {
window.removeEventListener('keydown', handler, true);
}
}, [handleAccept, handleReject]) // 添加handleAccept和handleReject作为依赖项
}, [handleAccept, handleReject]) // Dependencies for the effect
return (
<div id="infio-apply-view">
@@ -118,16 +133,16 @@ export default function ApplyViewRoot({
aria-label="Accept changes"
onClick={handleAccept}
>
{acceptIcon && <CheckIcon size={14} />}
Accept {getShortcutText('accept')}
{acceptIcon && '✓'}
Accept All {getShortcutText('accept')}
</button>
<button
className="clickable-icon view-action infio-reject-button"
aria-label="Reject changes"
onClick={handleReject}
>
{rejectIcon && <X size={14} />}
Reject {getShortcutText('reject')}
{rejectIcon && '✗'}
Reject All {getShortcutText('reject')}
</button>
</div>
</div>
@@ -144,37 +159,119 @@ export default function ApplyViewRoot({
: ''}
</div>
{diff.map((part, index) => (
<div
key={index}
className={`infio-diff-line ${part.added ? 'added' : part.removed ? 'removed' : ''}`}
>
<div style={{ width: '100%' }}>{part.value}</div>
{(part.added || part.removed) && (
<div className="infio-diff-line-actions">
<button
aria-label="Accept line"
onClick={() => acceptDiffLine(index)}
className="infio-accept"
>
{acceptIcon && 'Y'}
</button>
<button
aria-label="Exclude line"
onClick={() => excludeDiffLine(index)}
className="infio-exclude"
>
{excludeIcon && 'N'}
</button>
{diff.map((part, index) => {
// Determine line display status based on diffStatus
const status = diffStatus[index]
const isHidden =
(part.added && status === 'excluded') ||
(part.removed && status === 'accepted')
if (isHidden) return null
return (
<div
key={index}
className={`infio-diff-line ${part.added ? 'added' : part.removed ? 'removed' : ''} ${status !== 'active' ? status : ''}`}
>
<div className="infio-diff-content-wrapper">
<ContentEditable
html={editedContents[index]}
onChange={(evt) => handleContentChange(index, evt)}
className="infio-editable-content"
/>
{(part.added || part.removed) && status === 'active' && (
<div className="infio-diff-line-actions">
<button
aria-label="Accept line"
onClick={() => acceptDiffLine(index)}
className="infio-accept"
>
{acceptIcon && '✓'}
</button>
<button
aria-label="Exclude line"
onClick={() => excludeDiffLine(index)}
className="infio-exclude"
>
{excludeIcon && '✗'}
</button>
</div>
)}
</div>
)}
</div>
))}
</div>
)
})}
</div>
</div>
</div>
</div>
</div>
<style>{`
.infio-diff-content-wrapper {
position: relative;
width: 100%;
}
.infio-editable-content {
width: 100%;
min-height: 1.2em;
padding: 4px;
padding-right: 60px;
border: 1px solid transparent;
box-sizing: border-box;
}
.infio-editable-content:focus {
outline: none;
border-color: var(--interactive-accent);
background-color: var(--background-primary);
}
.infio-diff-line-actions {
position: absolute;
right: 4px;
top: 4px;
display: flex;
gap: 4px;
}
.infio-diff-line-actions button {
padding: 2px 6px;
border-radius: 4px;
background: var(--background-secondary);
border: 1px solid var(--background-modifier-border);
cursor: pointer;
opacity: 0.7;
transition: opacity 0.2s;
}
.infio-diff-line-actions button:hover {
opacity: 1;
}
.infio-accept {
color: #26a69a;
}
.infio-exclude {
color: #ef5350;
}
.infio-diff-line.added .infio-editable-content {
background-color: rgba(0, 255, 0, 0.1);
border-left: 3px solid #26a69a;
}
.infio-diff-line.removed .infio-editable-content {
background-color: rgba(255, 0, 0, 0.1);
border-left: 3px solid #ef5350;
text-decoration: line-through;
}
.infio-diff-line.accepted .infio-editable-content {
opacity: 0.7;
}
`}</style>
</div>
)
}