Skip to content

Commit c835935

Browse files
committed
Adding split-file diffs
1 parent 249205b commit c835935

File tree

3 files changed

+281
-1
lines changed

3 files changed

+281
-1
lines changed

src/diff/file.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { MergeView } from '@codemirror/merge';
2+
import { basicSetup } from 'codemirror';
3+
import { EditorView } from '@codemirror/view';
4+
import { jupyterTheme } from '@jupyterlab/codemirror';
5+
import { Widget } from '@lumino/widgets';
6+
import type { IDocumentWidget } from '@jupyterlab/docregistry';
7+
import type { FileEditor } from '@jupyterlab/fileeditor';
8+
import type { TranslationBundle } from '@jupyterlab/translation';
9+
import type { CodeMirrorEditor } from '@jupyterlab/codemirror';
10+
11+
export interface ISplitFileDiffOptions {
12+
/**
13+
* The file editor widget (document widget) that contains the CodeMirror editor.
14+
* This is optional but helpful for toolbar placement or context.
15+
*/
16+
fileEditorWidget?: IDocumentWidget<FileEditor>;
17+
18+
/**
19+
* The CodeMirrorEditor instance for the file being compared
20+
*/
21+
editor: CodeMirrorEditor;
22+
23+
/**
24+
* Original source text
25+
*/
26+
originalSource: string;
27+
28+
/**
29+
* New / modified source text
30+
*/
31+
newSource: string;
32+
33+
/**
34+
* Translation bundle (optional, kept for parity with other APIs)
35+
*/
36+
trans?: TranslationBundle;
37+
38+
/**
39+
* Whether to open the diff immediately (defaults to true).
40+
*/
41+
openDiff?: boolean;
42+
}
43+
44+
/**
45+
* A Lumino widget that contains a CodeMirror MergeView (side-by-side)
46+
* for file diffs. This is view-only (both editors are non-editable).
47+
*/
48+
export class CodeMirrorSplitFileWidget extends Widget {
49+
private _originalCode: string;
50+
private _modifiedCode: string;
51+
private _mergeView: MergeView | null = null;
52+
private _openDiff: boolean;
53+
private _scrollWrapper: HTMLElement;
54+
55+
constructor(options: ISplitFileDiffOptions) {
56+
super();
57+
this.addClass('jp-SplitFileDiffView');
58+
this._originalCode = options.originalSource;
59+
this._modifiedCode = options.newSource;
60+
this._openDiff = options.openDiff ?? true;
61+
62+
this.node.style.display = 'flex';
63+
this.node.style.flexDirection = 'column';
64+
this.node.style.height = '100%';
65+
this.node.style.width = '100%';
66+
67+
this._scrollWrapper = document.createElement('div');
68+
this._scrollWrapper.classList.add('jp-SplitDiff-scroll');
69+
this.node.appendChild(this._scrollWrapper);
70+
}
71+
72+
protected onAfterAttach(): void {
73+
this._createSplitView();
74+
}
75+
76+
protected onBeforeDetach(): void {
77+
this._destroySplitView();
78+
}
79+
80+
private _createSplitView(): void {
81+
if (this._mergeView) {
82+
return;
83+
}
84+
85+
// MergeView options: left (a) = original, right (b) = modified
86+
//TODO: Currently Both panes are non-editable, but have to make right pane editable.
87+
this._mergeView = new MergeView({
88+
a: {
89+
doc: this._originalCode,
90+
extensions: [
91+
basicSetup,
92+
EditorView.editable.of(false),
93+
EditorView.lineWrapping,
94+
jupyterTheme
95+
]
96+
},
97+
b: {
98+
doc: this._modifiedCode,
99+
extensions: [
100+
basicSetup,
101+
EditorView.editable.of(false),
102+
EditorView.lineWrapping,
103+
jupyterTheme
104+
]
105+
},
106+
parent: this._scrollWrapper
107+
});
108+
109+
if (!this._openDiff) {
110+
this.hide();
111+
}
112+
}
113+
114+
private _destroySplitView(): void {
115+
if (this._mergeView) {
116+
try {
117+
this._mergeView.destroy();
118+
} catch (err) {
119+
// best-effort cleanup
120+
console.warn('Error destroying split-file merge view', err);
121+
}
122+
this._mergeView = null;
123+
}
124+
}
125+
126+
dispose(): void {
127+
this._destroySplitView();
128+
super.dispose();
129+
}
130+
}
131+
132+
/**
133+
* Factory to create a CodeMirrorSplitFileWidget.
134+
* Keep the signature async to match other factories and future expansion.
135+
*/
136+
export async function createCodeMirrorSplitFileWidget(
137+
options: ISplitFileDiffOptions
138+
): Promise<Widget> {
139+
const widget = new CodeMirrorSplitFileWidget(options);
140+
return widget;
141+
}

src/plugin.ts

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,8 +423,112 @@ const unifiedFileDiffPlugin: JupyterFrontEndPlugin<void> = {
423423
}
424424
};
425425

426+
/**
427+
* Split file diff plugin (side-by-side)
428+
*/
429+
const splitFileDiffPlugin: JupyterFrontEndPlugin<void> = {
430+
id: 'jupyterlab-diff:split-file-diff-plugin',
431+
description: 'Show file diff using side-by-side split view',
432+
requires: [IEditorTracker],
433+
optional: [ITranslator],
434+
autoStart: true,
435+
activate: async (
436+
app: JupyterFrontEnd,
437+
editorTracker: IEditorTracker,
438+
translator: ITranslator | null
439+
) => {
440+
const { commands } = app;
441+
const trans = (translator ?? nullTranslator).load(TRANSLATION_NAMESPACE);
442+
443+
commands.addCommand('jupyterlab-diff:split-file-diff', {
444+
label: trans.__('Diff File (Split view)'),
445+
describedBy: {
446+
args: {
447+
type: 'object',
448+
properties: {
449+
filePath: {
450+
type: 'string',
451+
description: trans.__(
452+
'Path to the file to diff. Defaults to current file in editor.'
453+
)
454+
},
455+
originalSource: {
456+
type: 'string',
457+
description: trans.__('Original source code to compare against')
458+
},
459+
newSource: {
460+
type: 'string',
461+
description: trans.__('New source code to compare with')
462+
},
463+
openDiff: {
464+
type: 'boolean',
465+
description: trans.__('Whether to open the diff automatically')
466+
}
467+
},
468+
required: ['originalSource', 'newSource']
469+
}
470+
},
471+
execute: async (args: any = {}) => {
472+
const { filePath, originalSource, newSource, openDiff = true } = args;
473+
474+
if (!originalSource || !newSource) {
475+
console.error(
476+
trans.__('Missing required arguments: originalSource and newSource')
477+
);
478+
return;
479+
}
480+
481+
// Resolve the file editor widget: prefer the one matching filePath if provided.
482+
let fileEditorWidget = editorTracker.currentWidget;
483+
if (filePath) {
484+
const found = editorTracker.find(widget => {
485+
return widget.context?.path === filePath;
486+
});
487+
if (found) {
488+
fileEditorWidget = found;
489+
}
490+
}
491+
492+
if (!fileEditorWidget) {
493+
console.error(trans.__('No editor found for the file'));
494+
return;
495+
}
496+
497+
// Grab the CodeMirrorEditor instance from the FileEditor content
498+
// FileEditor.content.editor should be the underlying editor instance.
499+
const editor = fileEditorWidget.content
500+
.editor as any as CodeMirrorEditor;
501+
if (!editor) {
502+
console.error(trans.__('No code editor found in the file widget'));
503+
return;
504+
}
505+
506+
// Create the split widget and add to main area
507+
const { createCodeMirrorSplitFileWidget } = await import('./diff/file');
508+
509+
const widget = await createCodeMirrorSplitFileWidget({
510+
fileEditorWidget,
511+
editor,
512+
originalSource,
513+
newSource,
514+
trans,
515+
openDiff
516+
});
517+
518+
widget.id = `jp-split-file-diff-${Date.now()}`;
519+
widget.title.label = `Split Diff: ${fileEditorWidget.title.label}`;
520+
widget.title.closable = true;
521+
522+
app.shell.add(widget, 'main');
523+
app.shell.activateById(widget.id);
524+
}
525+
});
526+
}
527+
};
528+
426529
export default [
427530
splitCellDiffPlugin,
428531
unifiedCellDiffPlugin,
429-
unifiedFileDiffPlugin
532+
unifiedFileDiffPlugin,
533+
splitFileDiffPlugin
430534
];

style/base.css

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,38 @@
7777
background-color: var(--jp-layout-color3);
7878
border-color: var(--jp-border-color1);
7979
}
80+
81+
/* Root container: allow layout to flex & scroll correctly */
82+
.jp-SplitFileDiffView {
83+
display: flex;
84+
flex-direction: column;
85+
height: 100%;
86+
width: 100%;
87+
min-height: 0;
88+
min-width: 0;
89+
background-color: var(--jp-layout-color0);
90+
color: var(--jp-ui-font-color1);
91+
}
92+
93+
/* Scroll container: hosts the side-by-side MergeView */
94+
.jp-SplitDiff-scroll {
95+
flex: 1 1 auto;
96+
overflow: auto;
97+
min-height: 0;
98+
}
99+
100+
/* CodeMirror internal scroll area */
101+
.jp-SplitDiff-scroll .cm-scroller {
102+
height: 100%;
103+
overflow: auto;
104+
}
105+
106+
/* Allow editor to shrink instead of expanding infinitely */
107+
.jp-SplitDiff-scroll .cm-editor {
108+
min-height: 0;
109+
}
110+
111+
/* Optional: line wrapping styling for readability on narrow layouts */
112+
.jp-SplitDiff-scroll .cm-content {
113+
white-space: pre;
114+
}

0 commit comments

Comments
 (0)