diff --git a/apps/desktop/package.json b/apps/desktop/package.json index a72fe284e2e..a1894df9deb 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -57,6 +57,7 @@ "@codemirror/lang-yaml": "^6.1.2", "@codemirror/language": "^6.12.2", "@codemirror/legacy-modes": "^6.5.2", + "@codemirror/merge": "^6.12.1", "@codemirror/search": "^6.6.0", "@codemirror/state": "^6.5.4", "@codemirror/theme-one-dark": "^6.1.3", diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/CodeMirrorDiffViewer/CodeMirrorDiffViewer.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/CodeMirrorDiffViewer/CodeMirrorDiffViewer.tsx new file mode 100644 index 00000000000..177dae7bfa5 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/CodeMirrorDiffViewer/CodeMirrorDiffViewer.tsx @@ -0,0 +1,132 @@ +import { defaultKeymap, indentWithTab } from "@codemirror/commands"; +import { MergeView } from "@codemirror/merge"; +import { highlightSelectionMatches, searchKeymap } from "@codemirror/search"; +import { Compartment, EditorState } from "@codemirror/state"; +import { + drawSelection, + EditorView, + highlightSpecialChars, + keymap, + lineNumbers, +} from "@codemirror/view"; +import { useEffect, useRef } from "react"; +import { electronTrpc } from "renderer/lib/electron-trpc"; +import { createCodeMirrorTheme } from "renderer/screens/main/components/WorkspaceView/components/CodeEditor/createCodeMirrorTheme"; +import { loadLanguageSupport } from "renderer/screens/main/components/WorkspaceView/components/CodeEditor/loadLanguageSupport"; +import { getCodeSyntaxHighlighting } from "renderer/screens/main/components/WorkspaceView/utils/code-theme"; +import { useResolvedTheme } from "renderer/stores/theme"; +import type { DiffViewMode } from "shared/changes-types"; + +interface CodeMirrorDiffViewerProps { + original: string; + modified: string; + language: string; + viewMode: DiffViewMode; +} + +export function CodeMirrorDiffViewer({ + original, + modified, + language, + viewMode, +}: CodeMirrorDiffViewerProps) { + const containerRef = useRef(null); + const mergeViewRef = useRef(null); + const langCompartmentA = useRef(new Compartment()).current; + const langCompartmentB = useRef(new Compartment()).current; + const themeCompartmentA = useRef(new Compartment()).current; + const themeCompartmentB = useRef(new Compartment()).current; + const activeTheme = useResolvedTheme(); + const { data: fontSettings } = electronTrpc.settings.getFontSettings.useQuery( + undefined, + { staleTime: 30_000 }, + ); + const editorFontFamily = fontSettings?.editorFontFamily ?? undefined; + const editorFontSize = fontSettings?.editorFontSize ?? undefined; + + // biome-ignore lint/correctness/useExhaustiveDependencies: MergeView is created once and destroyed on unmount + useEffect(() => { + if (!containerRef.current) return; + + const baseExtensions = [ + lineNumbers(), + highlightSpecialChars(), + drawSelection(), + highlightSelectionMatches(), + EditorState.readOnly.of(true), + EditorView.editable.of(false), + EditorView.lineWrapping, + keymap.of([indentWithTab, ...defaultKeymap, ...searchKeymap]), + ]; + + const themeExts = [ + getCodeSyntaxHighlighting(activeTheme), + createCodeMirrorTheme( + activeTheme, + { fontFamily: editorFontFamily, fontSize: editorFontSize }, + true, + ), + ]; + + const mergeView = new MergeView({ + parent: containerRef.current, + collapseUnchanged: { margin: 3, minSize: 4 }, + diffConfig: { scanLimit: 50000, timeout: 5000 }, + a: { + doc: original, + extensions: [ + ...baseExtensions, + themeCompartmentA.of(themeExts), + langCompartmentA.of([]), + ], + }, + b: { + doc: modified, + extensions: [ + ...baseExtensions, + themeCompartmentB.of(themeExts), + langCompartmentB.of([]), + ], + }, + }); + + mergeViewRef.current = mergeView; + + void loadLanguageSupport(language).then((ext) => { + if (!ext || !mergeViewRef.current) return; + const mv = mergeViewRef.current; + mv.a.dispatch({ effects: langCompartmentA.reconfigure(ext) }); + mv.b.dispatch({ effects: langCompartmentB.reconfigure(ext) }); + }); + + return () => { + mergeView.destroy(); + mergeViewRef.current = null; + }; + }, [original, modified, language, viewMode]); + + useEffect(() => { + const mv = mergeViewRef.current; + if (!mv) return; + + const themeExts = [ + getCodeSyntaxHighlighting(activeTheme), + createCodeMirrorTheme( + activeTheme, + { fontFamily: editorFontFamily, fontSize: editorFontSize }, + true, + ), + ]; + + mv.a.dispatch({ effects: themeCompartmentA.reconfigure(themeExts) }); + mv.b.dispatch({ effects: themeCompartmentB.reconfigure(themeExts) }); + }, [ + activeTheme, + editorFontFamily, + editorFontSize, + themeCompartmentA, + themeCompartmentB, + ]); + + return
; +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/CodeMirrorDiffViewer/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/CodeMirrorDiffViewer/index.ts new file mode 100644 index 00000000000..13fcbde4860 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/CodeMirrorDiffViewer/index.ts @@ -0,0 +1 @@ +export { CodeMirrorDiffViewer } from "./CodeMirrorDiffViewer"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileViewerContent/FileViewerContent.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileViewerContent/FileViewerContent.tsx index 0148172700f..61c7e13eedd 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileViewerContent/FileViewerContent.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileViewerContent/FileViewerContent.tsx @@ -18,6 +18,7 @@ import { detectLanguage } from "shared/detect-language"; import { isImageFile, isSpreadsheetFile } from "shared/file-types"; import type { FileViewerMode } from "shared/tabs-types"; import { useScrollToFirstDiffChange } from "../../hooks/useScrollToFirstDiffChange"; +import { CodeMirrorDiffViewer } from "../CodeMirrorDiffViewer"; import { DiffScrollbarDecorations } from "../DiffScrollbarDecorations"; import { DiffViewerContextMenu } from "../DiffViewerContextMenu"; import { FileEditorContextMenu } from "../FileEditorContextMenu"; @@ -319,6 +320,11 @@ export function FileViewerContent({ ); } + const totalLines = + diffData.original.split("\n").length + + diffData.modified.split("\n").length; + const useLargeDiffViewer = totalLines > 2000; + return (
- + {!useLargeDiffViewer && ( + + )}
{ + if (useLargeDiffViewer) return; const location = getDiffLocationFromEvent(event.nativeEvent); if (!location) { return; @@ -386,16 +395,27 @@ export function FileViewerContent({ }; }} > - + {useLargeDiffViewer ? ( + + ) : ( + + )}
- + {!useLargeDiffViewer && ( + + )}
); diff --git a/bun.lock b/bun.lock index aefab368d8d..25e3c5bef40 100644 --- a/bun.lock +++ b/bun.lock @@ -134,6 +134,7 @@ "@codemirror/lang-yaml": "^6.1.2", "@codemirror/language": "^6.12.2", "@codemirror/legacy-modes": "^6.5.2", + "@codemirror/merge": "^6.12.1", "@codemirror/search": "^6.6.0", "@codemirror/state": "^6.5.4", "@codemirror/theme-one-dark": "^6.1.3", @@ -1295,6 +1296,8 @@ "@codemirror/lint": ["@codemirror/lint@6.9.5", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.35.0", "crelt": "^1.0.5" } }, "sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA=="], + "@codemirror/merge": ["@codemirror/merge@6.12.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/highlight": "^1.0.0", "style-mod": "^4.1.0" } }, "sha512-GA8hBq2T+IFM0sb5fk8CunTrqOulA3zurJmHtzcU15EMnL8aYpVINfJ5bkfd53M4ikwoew4Y1ydtSaAlk6+B1w=="], + "@codemirror/search": ["@codemirror/search@6.6.0", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.37.0", "crelt": "^1.0.5" } }, "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw=="], "@codemirror/state": ["@codemirror/state@6.5.4", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw=="],