Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(null);
const mergeViewRef = useRef<MergeView | null>(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) });
});
Comment thread
MocA-Love marked this conversation as resolved.

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 <div ref={containerRef} className="h-full w-full overflow-auto" />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { CodeMirrorDiffViewer } from "./CodeMirrorDiffViewer";
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -319,6 +320,11 @@ export function FileViewerContent({
);
}

const totalLines =
diffData.original.split("\n").length +
diffData.modified.split("\n").length;
const useLargeDiffViewer = totalLines > 2000;

return (
<DiffViewerContextMenu
containerRef={diffContainerRef}
Expand Down Expand Up @@ -347,18 +353,20 @@ export function FileViewerContent({
}}
>
<div className="relative h-full">
<MarkdownSearch
isOpen={diffSearch.isSearchOpen}
query={diffSearch.query}
caseSensitive={diffSearch.caseSensitive}
matchCount={diffSearch.matchCount}
activeMatchIndex={diffSearch.activeMatchIndex}
onQueryChange={diffSearch.setQuery}
onCaseSensitiveChange={diffSearch.setCaseSensitive}
onFindNext={diffSearch.findNext}
onFindPrevious={diffSearch.findPrevious}
onClose={diffSearch.closeSearch}
/>
{!useLargeDiffViewer && (
<MarkdownSearch
isOpen={diffSearch.isSearchOpen}
query={diffSearch.query}
caseSensitive={diffSearch.caseSensitive}
matchCount={diffSearch.matchCount}
activeMatchIndex={diffSearch.activeMatchIndex}
onQueryChange={diffSearch.setQuery}
onCaseSensitiveChange={diffSearch.setCaseSensitive}
onFindNext={diffSearch.findNext}
onFindPrevious={diffSearch.findPrevious}
onClose={diffSearch.closeSearch}
/>
)}
<div
ref={diffContainerRef}
className="h-full min-h-0 overflow-auto bg-background select-text"
Expand All @@ -368,6 +376,7 @@ export function FileViewerContent({
}
}}
onContextMenuCapture={(event) => {
if (useLargeDiffViewer) return;
const location = getDiffLocationFromEvent(event.nativeEvent);
if (!location) {
return;
Expand All @@ -386,16 +395,27 @@ export function FileViewerContent({
};
}}
>
<LightDiffViewer
key={filePath}
contents={diffData}
viewMode={diffViewMode}
hideUnchangedRegions={hideUnchangedRegions}
filePath={filePath}
className="min-h-full"
/>
{useLargeDiffViewer ? (
<CodeMirrorDiffViewer
original={diffData.original}
modified={diffData.modified}
language={diffData.language}
viewMode={diffViewMode}
/>
) : (
<LightDiffViewer
key={filePath}
contents={diffData}
viewMode={diffViewMode}
hideUnchangedRegions={hideUnchangedRegions}
filePath={filePath}
className="min-h-full"
/>
)}
</div>
<DiffScrollbarDecorations scrollContainerRef={diffContainerRef} />
{!useLargeDiffViewer && (
<DiffScrollbarDecorations scrollContainerRef={diffContainerRef} />
)}
</div>
</DiffViewerContextMenu>
);
Expand Down
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.