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

@ -52,6 +52,9 @@
},
"dependencies": {
"@anthropic-ai/sdk": "^0.27.3",
"@codemirror/basic-setup": "^0.20.0",
"@codemirror/lang-markdown": "^6.3.2",
"@codemirror/merge": "^6.10.0",
"@electric-sql/pglite": "0.2.14",
"@google/generative-ai": "^0.21.0",
"@langchain/core": "^0.3.26",
@ -95,6 +98,7 @@
"parse5": "^7.1.2",
"path": "^0.12.7",
"react": "^18.3.1",
"react-contenteditable": "^3.3.7",
"react-dom": "^18.3.1",
"react-markdown": "^9.0.1",
"react-syntax-highlighter": "^15.5.0",

273
pnpm-lock.yaml generated
View File

@ -11,6 +11,15 @@ importers:
'@anthropic-ai/sdk':
specifier: ^0.27.3
version: 0.27.3
'@codemirror/basic-setup':
specifier: ^0.20.0
version: 0.20.0
'@codemirror/lang-markdown':
specifier: ^6.3.2
version: 6.3.2
'@codemirror/merge':
specifier: ^6.10.0
version: 6.10.0
'@electric-sql/pglite':
specifier: 0.2.14
version: 0.2.14
@ -140,6 +149,9 @@ importers:
react:
specifier: ^18.3.1
version: 18.3.1
react-contenteditable:
specifier: ^3.3.7
version: 3.3.7(react@18.3.1)
react-dom:
specifier: ^18.3.1
version: 18.3.1(react@18.3.1)
@ -445,9 +457,58 @@ packages:
'@cfworker/json-schema@4.1.1':
resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==}
'@codemirror/autocomplete@0.20.3':
resolution: {integrity: sha512-lYB+NPGP+LEzAudkWhLfMxhTrxtLILGl938w+RcFrGdrIc54A+UgmCoz+McE3IYRFp4xyQcL4uFJwo+93YdgHw==}
'@codemirror/autocomplete@6.18.6':
resolution: {integrity: sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==}
'@codemirror/basic-setup@0.20.0':
resolution: {integrity: sha512-W/ERKMLErWkrVLyP5I8Yh8PXl4r+WFNkdYVSzkXYPQv2RMPSkWpr2BgggiSJ8AHF/q3GuApncDD8I4BZz65fyg==}
deprecated: In version 6.0, this package has been renamed to just 'codemirror'
'@codemirror/commands@0.20.0':
resolution: {integrity: sha512-v9L5NNVA+A9R6zaFvaTbxs30kc69F6BkOoiEbeFw4m4I0exmDEKBILN6mK+GksJtvTzGBxvhAPlVFTdQW8GB7Q==}
'@codemirror/lang-css@6.3.1':
resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==}
'@codemirror/lang-html@6.4.9':
resolution: {integrity: sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==}
'@codemirror/lang-javascript@6.2.3':
resolution: {integrity: sha512-8PR3vIWg7pSu7ur8A07pGiYHgy3hHj+mRYRCSG8q+mPIrl0F02rgpGv+DsQTHRTc30rydOsf5PZ7yjKFg2Ackw==}
'@codemirror/lang-markdown@6.3.2':
resolution: {integrity: sha512-c/5MYinGbFxYl4itE9q/rgN/sMTjOr8XL5OWnC+EaRMLfCbVUmmubTJfdgpfcSS2SCaT7b+Q+xi3l6CgoE+BsA==}
'@codemirror/language@0.20.2':
resolution: {integrity: sha512-WB3Bnuusw0xhVvhBocieYKwJm04SOk5bPoOEYksVHKHcGHFOaYaw+eZVxR4gIqMMcGzOIUil0FsCmFk8yrhHpw==}
'@codemirror/language@6.11.0':
resolution: {integrity: sha512-A7+f++LodNNc1wGgoRDTt78cOwWm9KVezApgjOMp1W4hM0898nsqBXwF+sbePE7ZRcjN7Sa1Z5m2oN27XkmEjQ==}
'@codemirror/lint@0.20.3':
resolution: {integrity: sha512-06xUScbbspZ8mKoODQCEx6hz1bjaq9m8W8DxdycWARMiiX1wMtfCh/MoHpaL7ws/KUMwlsFFfp2qhm32oaCvVA==}
'@codemirror/lint@6.8.5':
resolution: {integrity: sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==}
'@codemirror/merge@6.10.0':
resolution: {integrity: sha512-Omn0gU6MM5cKQGqgKoIhFjUqCNWH/nukCMLXzu/1jOdtiHsxAu3GdENBf1QYkoIC3FSgkF7X/ClOqYeUATQ4Sw==}
'@codemirror/search@0.20.1':
resolution: {integrity: sha512-ROe6gRboQU5E4z6GAkNa2kxhXqsGNbeLEisbvzbOeB7nuDYXUZ70vGIgmqPu0tB+1M3F9yWk6W8k2vrFpJaD4Q==}
'@codemirror/state@0.20.1':
resolution: {integrity: sha512-ms0tlV5A02OK0pFvTtSUGMLkoarzh1F8mr6jy1cD7ucSC2X/VLHtQCxfhdSEGqTYlQF2hoZtmLv+amqhdgbwjQ==}
'@codemirror/state@6.5.2':
resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==}
'@codemirror/view@0.20.7':
resolution: {integrity: sha512-pqEPCb9QFTOtHgAH5XU/oVy9UR/Anj6r+tG5CRmkNVcqSKEPmBU05WtN/jxJCFZBXf6HumzWC9ydE4qstO3TxQ==}
'@codemirror/view@6.36.2':
resolution: {integrity: sha512-DZ6ONbs8qdJK0fdN7AB82CgI6tYXf4HWk1wSVa0+9bhVznCuuvhQtX8bFBoy3dv8rZSQqUd8GvhVAcielcidrA==}
@ -1302,6 +1363,36 @@ packages:
peerDependencies:
yjs: '>=13.5.22'
'@lezer/common@0.16.1':
resolution: {integrity: sha512-qPmG7YTZ6lATyTOAWf8vXE+iRrt1NJd4cm2nJHK+v7X9TsOF6+HtuU/ctaZy2RCrluxDb89hI6KWQ5LfQGQWuA==}
'@lezer/common@1.2.3':
resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==}
'@lezer/css@1.1.11':
resolution: {integrity: sha512-FuAnusbLBl1SEAtfN8NdShxYJiESKw9LAFysfea1T96jD3ydBn12oYjaSG1a04BQRIUd93/0D8e5CV1cUMkmQg==}
'@lezer/highlight@0.16.0':
resolution: {integrity: sha512-iE5f4flHlJ1g1clOStvXNLbORJoiW4Kytso6ubfYzHnaNo/eo5SKhxs4wv/rtvwZQeZrK3we8S9SyA7OGOoRKQ==}
'@lezer/highlight@1.2.1':
resolution: {integrity: sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==}
'@lezer/html@1.3.10':
resolution: {integrity: sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==}
'@lezer/javascript@1.5.1':
resolution: {integrity: sha512-ATOImjeVJuvgm3JQ/bpo2Tmv55HSScE2MTPnKRMRIPx2cLhHGyX2VnqpHhtIV1tVzIjZDbcWQm+NCTF40ggZVw==}
'@lezer/lr@0.16.3':
resolution: {integrity: sha512-pau7um4eAw94BEuuShUIeQDTf3k4Wt6oIUOYxMmkZgDHdqtIcxWND4LRxi8nI9KuT4I1bXQv67BCapkxt7Ywqw==}
'@lezer/lr@1.4.2':
resolution: {integrity: sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==}
'@lezer/markdown@1.4.2':
resolution: {integrity: sha512-iYewCigG/517D0xJPQd7RGaCjZAFwROiH8T9h7OTtz0bRVtkxzFhGBFJ9JGKgBBs4uuo1cvxzyQ5iKhDLMcLUQ==}
'@libsql/client-wasm@0.14.0':
resolution: {integrity: sha512-gB/jtz0xuwrqAHApBv9e9JSew2030Fhj2edyZ83InZ4yPj/Q2LTUlEhaspEYT0T0xsAGqPy38uGrmq/OGS+DdQ==}
bundledDependencies:
@ -2218,6 +2309,9 @@ packages:
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
hasBin: true
crelt@1.0.6:
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@ -4153,6 +4247,11 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
react-contenteditable@3.3.7:
resolution: {integrity: sha512-GA9NbC0DkDdpN3iGvib/OMHWTJzDX2cfkgy5Tt98JJAbA3kLnyrNbBIpsSpPpq7T8d3scD39DHP+j8mAM7BIfQ==}
peerDependencies:
react: '>=16.3'
react-dom@18.3.1:
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
peerDependencies:
@ -5080,10 +5179,133 @@ snapshots:
'@cfworker/json-schema@4.1.1': {}
'@codemirror/autocomplete@0.20.3':
dependencies:
'@codemirror/language': 0.20.2
'@codemirror/state': 0.20.1
'@codemirror/view': 0.20.7
'@lezer/common': 0.16.1
'@codemirror/autocomplete@6.18.6':
dependencies:
'@codemirror/language': 6.11.0
'@codemirror/state': 6.5.2
'@codemirror/view': 6.36.2
'@lezer/common': 1.2.3
'@codemirror/basic-setup@0.20.0':
dependencies:
'@codemirror/autocomplete': 0.20.3
'@codemirror/commands': 0.20.0
'@codemirror/language': 0.20.2
'@codemirror/lint': 0.20.3
'@codemirror/search': 0.20.1
'@codemirror/state': 0.20.1
'@codemirror/view': 0.20.7
'@codemirror/commands@0.20.0':
dependencies:
'@codemirror/language': 0.20.2
'@codemirror/state': 0.20.1
'@codemirror/view': 0.20.7
'@lezer/common': 0.16.1
'@codemirror/lang-css@6.3.1':
dependencies:
'@codemirror/autocomplete': 6.18.6
'@codemirror/language': 6.11.0
'@codemirror/state': 6.5.2
'@lezer/common': 1.2.3
'@lezer/css': 1.1.11
'@codemirror/lang-html@6.4.9':
dependencies:
'@codemirror/autocomplete': 6.18.6
'@codemirror/lang-css': 6.3.1
'@codemirror/lang-javascript': 6.2.3
'@codemirror/language': 6.11.0
'@codemirror/state': 6.5.2
'@codemirror/view': 6.36.2
'@lezer/common': 1.2.3
'@lezer/css': 1.1.11
'@lezer/html': 1.3.10
'@codemirror/lang-javascript@6.2.3':
dependencies:
'@codemirror/autocomplete': 6.18.6
'@codemirror/language': 6.11.0
'@codemirror/lint': 6.8.5
'@codemirror/state': 6.5.2
'@codemirror/view': 6.36.2
'@lezer/common': 1.2.3
'@lezer/javascript': 1.5.1
'@codemirror/lang-markdown@6.3.2':
dependencies:
'@codemirror/autocomplete': 6.18.6
'@codemirror/lang-html': 6.4.9
'@codemirror/language': 6.11.0
'@codemirror/state': 6.5.2
'@codemirror/view': 6.36.2
'@lezer/common': 1.2.3
'@lezer/markdown': 1.4.2
'@codemirror/language@0.20.2':
dependencies:
'@codemirror/state': 0.20.1
'@codemirror/view': 0.20.7
'@lezer/common': 0.16.1
'@lezer/highlight': 0.16.0
'@lezer/lr': 0.16.3
style-mod: 4.1.2
'@codemirror/language@6.11.0':
dependencies:
'@codemirror/state': 6.5.2
'@codemirror/view': 6.36.2
'@lezer/common': 1.2.3
'@lezer/highlight': 1.2.1
'@lezer/lr': 1.4.2
style-mod: 4.1.2
'@codemirror/lint@0.20.3':
dependencies:
'@codemirror/state': 0.20.1
'@codemirror/view': 0.20.7
crelt: 1.0.6
'@codemirror/lint@6.8.5':
dependencies:
'@codemirror/state': 6.5.2
'@codemirror/view': 6.36.2
crelt: 1.0.6
'@codemirror/merge@6.10.0':
dependencies:
'@codemirror/language': 6.11.0
'@codemirror/state': 6.5.2
'@codemirror/view': 6.36.2
'@lezer/highlight': 1.2.1
style-mod: 4.1.2
'@codemirror/search@0.20.1':
dependencies:
'@codemirror/state': 0.20.1
'@codemirror/view': 0.20.7
crelt: 1.0.6
'@codemirror/state@0.20.1': {}
'@codemirror/state@6.5.2':
dependencies:
'@marijn/find-cluster-break': 1.0.2
'@codemirror/view@0.20.7':
dependencies:
'@codemirror/state': 0.20.1
style-mod: 4.1.2
w3c-keyname: 2.2.8
'@codemirror/view@6.36.2':
dependencies:
'@codemirror/state': 6.5.2
@ -5873,6 +6095,49 @@ snapshots:
lexical: 0.17.1
yjs: 13.6.23
'@lezer/common@0.16.1': {}
'@lezer/common@1.2.3': {}
'@lezer/css@1.1.11':
dependencies:
'@lezer/common': 1.2.3
'@lezer/highlight': 1.2.1
'@lezer/lr': 1.4.2
'@lezer/highlight@0.16.0':
dependencies:
'@lezer/common': 0.16.1
'@lezer/highlight@1.2.1':
dependencies:
'@lezer/common': 1.2.3
'@lezer/html@1.3.10':
dependencies:
'@lezer/common': 1.2.3
'@lezer/highlight': 1.2.1
'@lezer/lr': 1.4.2
'@lezer/javascript@1.5.1':
dependencies:
'@lezer/common': 1.2.3
'@lezer/highlight': 1.2.1
'@lezer/lr': 1.4.2
'@lezer/lr@0.16.3':
dependencies:
'@lezer/common': 0.16.1
'@lezer/lr@1.4.2':
dependencies:
'@lezer/common': 1.2.3
'@lezer/markdown@1.4.2':
dependencies:
'@lezer/common': 1.2.3
'@lezer/highlight': 1.2.1
'@libsql/client-wasm@0.14.0':
dependencies:
'@libsql/core': 0.14.0
@ -6897,6 +7162,8 @@ snapshots:
- supports-color
- ts-node
crelt@1.0.6: {}
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@ -9372,6 +9639,12 @@ snapshots:
queue-microtask@1.2.3: {}
react-contenteditable@3.3.7(react@18.3.1):
dependencies:
fast-deep-equal: 3.1.3
prop-types: 15.8.1
react: 18.3.1
react-dom@18.3.1(react@18.3.1):
dependencies:
loose-envify: 1.4.0

View File

@ -2,6 +2,7 @@ import { TFile, View, WorkspaceLeaf } from 'obsidian'
import { Root, createRoot } from 'react-dom/client'
import ApplyViewRoot from './components/apply-view/ApplyViewRoot'
// import DiffViewRoot from './components/apply-view/DiffViewRoot'
import { APPLY_VIEW_TYPE } from './constants'
import { AppProvider } from './contexts/AppContext'

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>
)
}