diff --git a/package.json b/package.json index 37055f7..c19117f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b985d30..f7cdccf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/ApplyView.tsx b/src/ApplyView.tsx index 9bb4366..2576e50 100644 --- a/src/ApplyView.tsx +++ b/src/ApplyView.tsx @@ -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' diff --git a/src/components/apply-view/ApplyViewRoot.tsx b/src/components/apply-view/ApplyViewRoot.tsx index f3f674c..d941adb 100644 --- a/src/components/apply-view/ApplyViewRoot.tsx +++ b/src/components/apply-view/ApplyViewRoot.tsx @@ -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( - diffLines(state.oldContent, state.newContent), - ) + // Track which lines have been accepted or excluded + const [diffStatus, setDiffStatus] = useState>([]) + + const [diff] = useState(() => { + 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( + 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 (
@@ -118,16 +133,16 @@ export default function ApplyViewRoot({ aria-label="Accept changes" onClick={handleAccept} > - {acceptIcon && } - Accept {getShortcutText('accept')} + {acceptIcon && '✓'} + Accept All {getShortcutText('accept')}
@@ -144,37 +159,119 @@ export default function ApplyViewRoot({ : ''} - {diff.map((part, index) => ( -
-
{part.value}
- {(part.added || part.removed) && ( -
- - + {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 ( +
+
+ handleContentChange(index, evt)} + className="infio-editable-content" + /> + {(part.added || part.removed) && status === 'active' && ( +
+ + +
+ )}
- )} -
- ))} +
+ ) + })}
+ ) }