xtralab 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/lib/git/diffProvider.js +94 -2
- package/lib/git/diffSurface.d.ts +29 -0
- package/lib/git/diffSurface.js +72 -8
- package/lib/git/diffWidget.d.ts +20 -1
- package/lib/git/diffWidget.js +107 -6
- package/package.json +1 -1
- package/src/git/diffProvider.tsx +131 -0
- package/src/git/diffSurface.tsx +144 -8
- package/src/git/diffWidget.tsx +141 -4
- package/style/base.css +1 -0
- package/style/git.css +29 -4
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+

|
|
2
2
|
|
|
3
3
|
An opinionated JupyterLab meta-package for use with coding agents.
|
|
4
4
|
|
|
@@ -38,9 +38,10 @@ build instructions.
|
|
|
38
38
|
|
|
39
39
|
xtralab builds on [`ajlab`](https://github.com/jtpio/ajlab), the agent-ready
|
|
40
40
|
JupyterLab base. On top of it, xtralab pulls in JupyterLab 4.6+ with
|
|
41
|
-
`jupyterlab-git`, `jupyterlab-lsp`, `jupyterlab-quickopen`,
|
|
42
|
-
and a set of light and dark
|
|
43
|
-
`jupyterlab-cursor-
|
|
41
|
+
`jupyterlab-git`, `jupyterlab-lsp`, `jupyterlab-quickopen`,
|
|
42
|
+
`jupyterlab-search-replace`, `jupyterlab-vim`, and a set of light and dark
|
|
43
|
+
themes (`jupyterlab-cursor-light`, `jupyterlab-cursor-dark`,
|
|
44
|
+
`jupyterlab-day`, `jupyterlab-night`). See
|
|
44
45
|
[`pyproject.toml`](./pyproject.toml) for the full list and pinned versions.
|
|
45
46
|
|
|
46
47
|
The bundled xtralab labextension then adds:
|
package/lib/git/diffProvider.js
CHANGED
|
@@ -6,7 +6,7 @@ import { ReactWidget } from '@jupyterlab/ui-components';
|
|
|
6
6
|
import { Git, IGitExtension } from '@jupyterlab/git';
|
|
7
7
|
import { PromiseDelegate } from '@lumino/coreutils';
|
|
8
8
|
import { Signal } from '@lumino/signaling';
|
|
9
|
-
import { DIFF_WIDGET_CSS_CLASS, DiffSurface, NotebookViewModeControl, isDarkTheme, readStoredNotebookViewMode, writeStoredNotebookViewMode } from './diffSurface';
|
|
9
|
+
import { DIFF_WIDGET_CSS_CLASS, DiffStyleControl, DiffSurface, NotebookViewModeControl, isDarkTheme, readStoredDiffStyle, readStoredNotebookViewMode, writeStoredDiffStyle, writeStoredNotebookViewMode } from './diffSurface';
|
|
10
10
|
import { IMAGE_DIFF_EXTENSIONS, imageDataType } from './imageDiff';
|
|
11
11
|
/**
|
|
12
12
|
* A `Git.Diff.IDiffWidget` that renders through {@link DiffSurface}.
|
|
@@ -25,9 +25,13 @@ class XtralabDiffWidget extends ReactWidget {
|
|
|
25
25
|
this._notebookViewModeChanged = new Signal(this);
|
|
26
26
|
this._hasNotebookView = false;
|
|
27
27
|
this._hasNotebookViewChanged = new Signal(this);
|
|
28
|
+
this._diffStyleChanged = new Signal(this);
|
|
29
|
+
this._fileDiffActive = false;
|
|
30
|
+
this._fileDiffActiveChanged = new Signal(this);
|
|
28
31
|
this._model = model;
|
|
29
32
|
this._context = context;
|
|
30
33
|
this._notebookViewMode = readStoredNotebookViewMode();
|
|
34
|
+
this._diffStyle = readStoredDiffStyle();
|
|
31
35
|
// `jp-git-diff-root` borrows jupyterlab-git's host sizing (height:100%
|
|
32
36
|
// flex column inside its PreviewMainAreaWidget); `jp-xtralab-DiffWidget`
|
|
33
37
|
// brings our diff theming + the `.jp-xtralab-DiffWidget-*` styles.
|
|
@@ -102,6 +106,39 @@ class XtralabDiffWidget extends ReactWidget {
|
|
|
102
106
|
get hasNotebookViewChanged() {
|
|
103
107
|
return this._hasNotebookViewChanged;
|
|
104
108
|
}
|
|
109
|
+
/** The user's current split-vs-unified layout choice for file diffs. */
|
|
110
|
+
get diffStyle() {
|
|
111
|
+
return this._diffStyle;
|
|
112
|
+
}
|
|
113
|
+
setDiffStyle(style) {
|
|
114
|
+
if (style === this._diffStyle) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
this._diffStyle = style;
|
|
118
|
+
writeStoredDiffStyle(style);
|
|
119
|
+
this._diffStyleChanged.emit(style);
|
|
120
|
+
}
|
|
121
|
+
get diffStyleChanged() {
|
|
122
|
+
return this._diffStyleChanged;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Whether the textual/code file diff (the only view the split-vs-unified
|
|
126
|
+
* choice affects) is currently active; mirrored into the toolbar so the
|
|
127
|
+
* Split/Unified selector only shows when it applies.
|
|
128
|
+
*/
|
|
129
|
+
get fileDiffActive() {
|
|
130
|
+
return this._fileDiffActive;
|
|
131
|
+
}
|
|
132
|
+
setFileDiffActive(value) {
|
|
133
|
+
if (value === this._fileDiffActive) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this._fileDiffActive = value;
|
|
137
|
+
this._fileDiffActiveChanged.emit(value);
|
|
138
|
+
}
|
|
139
|
+
get fileDiffActiveChanged() {
|
|
140
|
+
return this._fileDiffActiveChanged;
|
|
141
|
+
}
|
|
105
142
|
/** Resolve the in-flight {@link refresh} promise, if any. */
|
|
106
143
|
settleRefresh() {
|
|
107
144
|
const pending = this._pendingRefresh;
|
|
@@ -146,6 +183,22 @@ function ModelDiffView(props) {
|
|
|
146
183
|
const handleNotebookAvailabilityChange = React.useCallback((available) => {
|
|
147
184
|
widget.setHasNotebookView(available);
|
|
148
185
|
}, [widget]);
|
|
186
|
+
// Mirror the widget's diff-style choice so toolbar-driven changes
|
|
187
|
+
// re-render the diff. The widget owns the canonical value.
|
|
188
|
+
const [diffStyle, setDiffStyle] = React.useState(() => widget.diffStyle);
|
|
189
|
+
React.useEffect(() => {
|
|
190
|
+
const handler = (_sender, style) => {
|
|
191
|
+
setDiffStyle(style);
|
|
192
|
+
};
|
|
193
|
+
widget.diffStyleChanged.connect(handler);
|
|
194
|
+
setDiffStyle(widget.diffStyle);
|
|
195
|
+
return () => {
|
|
196
|
+
widget.diffStyleChanged.disconnect(handler);
|
|
197
|
+
};
|
|
198
|
+
}, [widget]);
|
|
199
|
+
const handleFileDiffActiveChange = React.useCallback((active) => {
|
|
200
|
+
widget.setFileDiffActive(active);
|
|
201
|
+
}, [widget]);
|
|
149
202
|
const [dark, setDark] = React.useState(() => isDarkTheme(themeManager));
|
|
150
203
|
React.useEffect(() => {
|
|
151
204
|
if (themeManager !== null) {
|
|
@@ -232,7 +285,7 @@ function ModelDiffView(props) {
|
|
|
232
285
|
void widget.refresh();
|
|
233
286
|
}
|
|
234
287
|
}), [canDiscardHunk, contentsManager, serverPath, widget]);
|
|
235
|
-
return (React.createElement(DiffSurface, { loading: state.loading, error: state.error, isBinary: false, oldText: state.oldText, newText: state.newText, newName: model.filename, oldName: model.filename, dark: dark, rendermime: rendermime, notebookViewMode: notebookViewMode, onNotebookAvailabilityChange: handleNotebookAvailabilityChange, hunkDiscard: hunkDiscard }));
|
|
288
|
+
return (React.createElement(DiffSurface, { loading: state.loading, error: state.error, isBinary: false, oldText: state.oldText, newText: state.newText, newName: model.filename, oldName: model.filename, dark: dark, rendermime: rendermime, notebookViewMode: notebookViewMode, diffStyle: diffStyle, onNotebookAvailabilityChange: handleNotebookAvailabilityChange, onFileDiffActiveChange: handleFileDiffActiveChange, hunkDiscard: hunkDiscard }));
|
|
236
289
|
}
|
|
237
290
|
/**
|
|
238
291
|
* Notebook/JSON toggle mounted into the diff toolbar that jupyterlab-git
|
|
@@ -271,6 +324,44 @@ function ToolbarControl(props) {
|
|
|
271
324
|
}, [widget]);
|
|
272
325
|
return (React.createElement(NotebookViewModeControl, { mode: mode, available: available, onChange: next => widget.setNotebookViewMode(next) }));
|
|
273
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* Split/Unified toggle mounted into the diff toolbar jupyterlab-git passes
|
|
329
|
+
* to the factory. Bound to the widget's signals so it stays in sync with the
|
|
330
|
+
* rendered surface and only appears while the textual/code file diff is the
|
|
331
|
+
* active view.
|
|
332
|
+
*/
|
|
333
|
+
class DiffStyleToolbarItem extends ReactWidget {
|
|
334
|
+
constructor(widget) {
|
|
335
|
+
super();
|
|
336
|
+
this._widget = widget;
|
|
337
|
+
this.addClass('jp-xtralab-DiffWidget-diffStyleToolbarItem');
|
|
338
|
+
}
|
|
339
|
+
render() {
|
|
340
|
+
return React.createElement(DiffStyleControlBound, { widget: this._widget });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function DiffStyleControlBound(props) {
|
|
344
|
+
const { widget } = props;
|
|
345
|
+
const [style, setStyle] = React.useState(() => widget.diffStyle);
|
|
346
|
+
const [available, setAvailable] = React.useState(() => widget.fileDiffActive);
|
|
347
|
+
React.useEffect(() => {
|
|
348
|
+
const onStyle = (_sender, next) => {
|
|
349
|
+
setStyle(next);
|
|
350
|
+
};
|
|
351
|
+
const onAvailable = (_sender, next) => {
|
|
352
|
+
setAvailable(next);
|
|
353
|
+
};
|
|
354
|
+
widget.diffStyleChanged.connect(onStyle);
|
|
355
|
+
widget.fileDiffActiveChanged.connect(onAvailable);
|
|
356
|
+
setStyle(widget.diffStyle);
|
|
357
|
+
setAvailable(widget.fileDiffActive);
|
|
358
|
+
return () => {
|
|
359
|
+
widget.diffStyleChanged.disconnect(onStyle);
|
|
360
|
+
widget.fileDiffActiveChanged.disconnect(onAvailable);
|
|
361
|
+
};
|
|
362
|
+
}, [widget]);
|
|
363
|
+
return (React.createElement(DiffStyleControl, { diffStyle: style, available: available, onChange: next => widget.setDiffStyle(next) }));
|
|
364
|
+
}
|
|
274
365
|
/**
|
|
275
366
|
* Build the `Git.Diff.Factory` xtralab registers with jupyterlab-git. The
|
|
276
367
|
* factory closes over the application-level context (contents manager for
|
|
@@ -282,6 +373,7 @@ function makeXtralabDiffFactory(context) {
|
|
|
282
373
|
const widget = new XtralabDiffWidget(options.model, context);
|
|
283
374
|
if (options.toolbar) {
|
|
284
375
|
options.toolbar.addItem('xtralab-notebook-view-mode', new NotebookViewModeToolbarItem(widget));
|
|
376
|
+
options.toolbar.addItem('xtralab-diff-style', new DiffStyleToolbarItem(widget));
|
|
285
377
|
}
|
|
286
378
|
return widget;
|
|
287
379
|
};
|
package/lib/git/diffSurface.d.ts
CHANGED
|
@@ -21,6 +21,14 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
|
21
21
|
* style the embedded `@pierre/diffs` viewer hang off this class.
|
|
22
22
|
*/
|
|
23
23
|
export declare const DIFF_WIDGET_CSS_CLASS = "jp-xtralab-DiffWidget";
|
|
24
|
+
/**
|
|
25
|
+
* Layout for textual/code diffs, forwarded to `@pierre/diffs` as its
|
|
26
|
+
* `diffStyle` option. `split` is the side-by-side old|new view; `unified`
|
|
27
|
+
* is the single-column inline view (what diffs.com calls "stacked").
|
|
28
|
+
*/
|
|
29
|
+
export type DiffStyle = 'split' | 'unified';
|
|
30
|
+
export declare function readStoredDiffStyle(): DiffStyle;
|
|
31
|
+
export declare function writeStoredDiffStyle(style: DiffStyle): void;
|
|
24
32
|
/**
|
|
25
33
|
* View modes available for `.ipynb` diffs. `notebook` is the cell-by-cell
|
|
26
34
|
* rendered view; `json` is the raw nbformat JSON file diff, for inspecting
|
|
@@ -81,11 +89,20 @@ export interface IDiffSurfaceProps {
|
|
|
81
89
|
rendermime: IRenderMimeRegistry | null;
|
|
82
90
|
/** Current rendered-vs-JSON choice for notebook diffs (host-controlled). */
|
|
83
91
|
notebookViewMode: NotebookDiffViewMode;
|
|
92
|
+
/** Split vs unified layout for the textual/code file diff (host-controlled). */
|
|
93
|
+
diffStyle: DiffStyle;
|
|
84
94
|
/**
|
|
85
95
|
* Called whenever the availability of a rendered notebook view changes,
|
|
86
96
|
* so the host can show/hide its Notebook/JSON toolbar toggle.
|
|
87
97
|
*/
|
|
88
98
|
onNotebookAvailabilityChange?: (available: boolean) => void;
|
|
99
|
+
/**
|
|
100
|
+
* Called whenever the textual/code file diff (the only view the split vs
|
|
101
|
+
* unified choice affects) becomes the active view, so the host can
|
|
102
|
+
* show/hide its Split/Unified toolbar toggle. False for image, binary,
|
|
103
|
+
* rendered-notebook, loading and error states.
|
|
104
|
+
*/
|
|
105
|
+
onFileDiffActiveChange?: (active: boolean) => void;
|
|
89
106
|
/**
|
|
90
107
|
* Called after each (re)computation of the diff with the number of hunks
|
|
91
108
|
* (or `null` when no textual diff exists). Hosts use this to implement
|
|
@@ -113,3 +130,15 @@ export declare function NotebookViewModeControl(props: {
|
|
|
113
130
|
available: boolean;
|
|
114
131
|
onChange: (mode: NotebookDiffViewMode) => void;
|
|
115
132
|
}): React.ReactElement;
|
|
133
|
+
/**
|
|
134
|
+
* Segmented Split/Unified selector. Mirrors {@link NotebookViewModeControl}:
|
|
135
|
+
* hosts mount it into their own toolbar and drive its value/visibility from
|
|
136
|
+
* the same state they pass to {@link DiffSurface}. Only meaningful while the
|
|
137
|
+
* textual/code file diff is the active view, so `available` mirrors the
|
|
138
|
+
* surface's `onFileDiffActiveChange`.
|
|
139
|
+
*/
|
|
140
|
+
export declare function DiffStyleControl(props: {
|
|
141
|
+
diffStyle: DiffStyle;
|
|
142
|
+
available: boolean;
|
|
143
|
+
onChange: (style: DiffStyle) => void;
|
|
144
|
+
}): React.ReactElement;
|
package/lib/git/diffSurface.js
CHANGED
|
@@ -79,6 +79,27 @@ function writeStoredSplitRatio(ratio) {
|
|
|
79
79
|
// See readStoredSplitRatio — best-effort persistence.
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
+
const DIFF_STYLE_STORAGE_KEY = 'xtralab:diff-style';
|
|
83
|
+
export function readStoredDiffStyle() {
|
|
84
|
+
try {
|
|
85
|
+
const raw = window.localStorage.getItem(DIFF_STYLE_STORAGE_KEY);
|
|
86
|
+
if (raw === 'split' || raw === 'unified') {
|
|
87
|
+
return raw;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (_a) {
|
|
91
|
+
// See readStoredSplitRatio — privacy-mode / sandboxed contexts can throw.
|
|
92
|
+
}
|
|
93
|
+
return 'split';
|
|
94
|
+
}
|
|
95
|
+
export function writeStoredDiffStyle(style) {
|
|
96
|
+
try {
|
|
97
|
+
window.localStorage.setItem(DIFF_STYLE_STORAGE_KEY, style);
|
|
98
|
+
}
|
|
99
|
+
catch (_a) {
|
|
100
|
+
// Best-effort.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
82
103
|
const NOTEBOOK_DIFF_VIEW_MODE_STORAGE_KEY = 'xtralab:notebook-diff-view-mode';
|
|
83
104
|
export function readStoredNotebookViewMode() {
|
|
84
105
|
try {
|
|
@@ -129,7 +150,7 @@ export function isDarkTheme(themeManager) {
|
|
|
129
150
|
* docstring for the division of labor between this surface and its hosts.
|
|
130
151
|
*/
|
|
131
152
|
export function DiffSurface(props) {
|
|
132
|
-
const { loading, error, isBinary, oldText, newText, newName, oldName, dark, rendermime, notebookViewMode, onNotebookAvailabilityChange, onMetadataChange, hunkDiscard } = props;
|
|
153
|
+
const { loading, error, isBinary, oldText, newText, newName, oldName, dark, rendermime, notebookViewMode, diffStyle, onNotebookAvailabilityChange, onFileDiffActiveChange, onMetadataChange, hunkDiscard } = props;
|
|
133
154
|
const hasContent = !loading && error === null && !isBinary;
|
|
134
155
|
// Raster images take a dedicated `<img>`-based view instead of a
|
|
135
156
|
// text/notebook diff; the host passes the two sides as base64 strings in
|
|
@@ -171,6 +192,19 @@ export function DiffSurface(props) {
|
|
|
171
192
|
hunkCount: metadata !== null ? metadata.hunks.length : null
|
|
172
193
|
});
|
|
173
194
|
}, [onMetadataChange, metadata]);
|
|
195
|
+
// Decide which view to render. The notebook view requires a successful
|
|
196
|
+
// nbformat parse; if that failed, we only show the raw JSON file diff.
|
|
197
|
+
// Computed before the early returns below so the file-diff-active effect
|
|
198
|
+
// can depend on it without violating the rules of hooks.
|
|
199
|
+
const hasNotebookView = notebookDiff !== null;
|
|
200
|
+
const showNotebookView = hasNotebookView && notebookViewMode === 'notebook';
|
|
201
|
+
const showFileDiff = !showNotebookView && metadata !== null;
|
|
202
|
+
// The split vs unified choice only affects the textual/code file diff, so
|
|
203
|
+
// tell the host whether that view is active to drive its Split/Unified
|
|
204
|
+
// toolbar toggle.
|
|
205
|
+
React.useEffect(() => {
|
|
206
|
+
onFileDiffActiveChange === null || onFileDiffActiveChange === void 0 ? void 0 : onFileDiffActiveChange(showFileDiff);
|
|
207
|
+
}, [onFileDiffActiveChange, showFileDiff]);
|
|
174
208
|
// Split ratio for the diff columns: fraction of width given to the
|
|
175
209
|
// deletions (left) pane. Persisted across sessions so the user only
|
|
176
210
|
// dials in their layout once.
|
|
@@ -283,11 +317,6 @@ export function DiffSurface(props) {
|
|
|
283
317
|
return (React.createElement("div", { className: "jp-xtralab-DiffWidget-content" },
|
|
284
318
|
React.createElement(ImageDiffView, { reference: oldText, challenger: newText, fileType: imageType })));
|
|
285
319
|
}
|
|
286
|
-
// Decide which view to render. The notebook view requires a successful
|
|
287
|
-
// nbformat parse; if that failed, we only show the raw JSON file diff.
|
|
288
|
-
const hasNotebookView = notebookDiff !== null;
|
|
289
|
-
const showNotebookView = hasNotebookView && notebookViewMode === 'notebook';
|
|
290
|
-
const showFileDiff = !showNotebookView && metadata !== null;
|
|
291
320
|
if (!showNotebookView && !showFileDiff) {
|
|
292
321
|
return (React.createElement("div", { className: "jp-xtralab-DiffWidget-status" }, "No content to diff."));
|
|
293
322
|
}
|
|
@@ -309,7 +338,7 @@ export function DiffSurface(props) {
|
|
|
309
338
|
// crashes on instantiation. Running on the main thread is
|
|
310
339
|
// fine for the small files git diffs typically operate on.
|
|
311
340
|
disableWorkerPool: true, options: {
|
|
312
|
-
diffStyle
|
|
341
|
+
diffStyle,
|
|
313
342
|
// Drop the file header — the tab title already shows the
|
|
314
343
|
// file name and the panel header carries the change
|
|
315
344
|
// context.
|
|
@@ -323,7 +352,7 @@ export function DiffSurface(props) {
|
|
|
323
352
|
// changes.
|
|
324
353
|
unsafeCSS: SPLIT_RESIZE_CSS
|
|
325
354
|
} })) : null),
|
|
326
|
-
showFileDiff ? (React.createElement("div", { className: "jp-xtralab-DiffWidget-resizer", style: { left: `${leftPercent}%` }, role: "separator", "aria-orientation": "vertical", "aria-valuenow": Math.round(leftPercent), "aria-valuemin": Math.round(MIN_SPLIT_RATIO * 100), "aria-valuemax": Math.round(MAX_SPLIT_RATIO * 100), title: "Drag to resize the diff panes (double-click to reset)", onPointerDown: handleResizerPointerDown, onDoubleClick: handleResizerDoubleClick })) : null)));
|
|
355
|
+
showFileDiff && diffStyle === 'split' ? (React.createElement("div", { className: "jp-xtralab-DiffWidget-resizer", style: { left: `${leftPercent}%` }, role: "separator", "aria-orientation": "vertical", "aria-valuenow": Math.round(leftPercent), "aria-valuemin": Math.round(MIN_SPLIT_RATIO * 100), "aria-valuemax": Math.round(MAX_SPLIT_RATIO * 100), title: "Drag to resize the diff panes (double-click to reset)", onPointerDown: handleResizerPointerDown, onDoubleClick: handleResizerDoubleClick })) : null)));
|
|
327
356
|
}
|
|
328
357
|
/**
|
|
329
358
|
* Segmented Notebook/JSON selector. Hosts mount this into whatever toolbar
|
|
@@ -340,3 +369,38 @@ export function NotebookViewModeControl(props) {
|
|
|
340
369
|
React.createElement("button", { type: "button", role: "tab", "aria-selected": mode === 'notebook', "data-active": mode === 'notebook', className: "jp-xtralab-DiffWidget-segmentedButton", onClick: () => onChange('notebook') }, "Notebook"),
|
|
341
370
|
React.createElement("button", { type: "button", role: "tab", "aria-selected": mode === 'json', "data-active": mode === 'json', className: "jp-xtralab-DiffWidget-segmentedButton", onClick: () => onChange('json') }, "JSON")));
|
|
342
371
|
}
|
|
372
|
+
/**
|
|
373
|
+
* Split / unified glyphs, inlined from the `@pierre/diffs` icon sprite
|
|
374
|
+
* (`diffs-icon-diff-split` / `diffs-icon-diff-unified`) so the toggle looks
|
|
375
|
+
* like the one on the library's own docs site without depending on the
|
|
376
|
+
* sprite sheet being injected into the document. `currentColor` lets the
|
|
377
|
+
* segmented-button styling drive the fill (including the active state).
|
|
378
|
+
*/
|
|
379
|
+
function DiffSplitIcon() {
|
|
380
|
+
return (React.createElement("svg", { viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": "true" },
|
|
381
|
+
React.createElement("path", { d: "M14 0H8.5v16H14a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2m-1.5 6.5v1h1a.5.5 0 0 1 0 1h-1v1a.5.5 0 0 1-1 0v-1h-1a.5.5 0 0 1 0-1h1v-1a.5.5 0 0 1 1 0" }),
|
|
382
|
+
React.createElement("path", { d: "M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h5.5V0zm.5 7.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1", opacity: ".3" })));
|
|
383
|
+
}
|
|
384
|
+
function DiffUnifiedIcon() {
|
|
385
|
+
return (React.createElement("svg", { viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": "true" },
|
|
386
|
+
React.createElement("path", { fillRule: "evenodd", d: "M16 14a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V8.5h16zm-8-4a.5.5 0 0 0-.5.5v1h-1a.5.5 0 0 0 0 1h1v1a.5.5 0 0 0 1 0v-1h1a.5.5 0 0 0 0-1h-1v-1A.5.5 0 0 0 8 10", clipRule: "evenodd" }),
|
|
387
|
+
React.createElement("path", { fillRule: "evenodd", d: "M14 0a2 2 0 0 1 2 2v5.5H0V2a2 2 0 0 1 2-2zM6.5 3.5a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1z", clipRule: "evenodd", opacity: ".4" })));
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Segmented Split/Unified selector. Mirrors {@link NotebookViewModeControl}:
|
|
391
|
+
* hosts mount it into their own toolbar and drive its value/visibility from
|
|
392
|
+
* the same state they pass to {@link DiffSurface}. Only meaningful while the
|
|
393
|
+
* textual/code file diff is the active view, so `available` mirrors the
|
|
394
|
+
* surface's `onFileDiffActiveChange`.
|
|
395
|
+
*/
|
|
396
|
+
export function DiffStyleControl(props) {
|
|
397
|
+
const { diffStyle, available, onChange } = props;
|
|
398
|
+
if (!available) {
|
|
399
|
+
return React.createElement(React.Fragment, null);
|
|
400
|
+
}
|
|
401
|
+
return (React.createElement("div", { className: "jp-xtralab-DiffWidget-segmented", role: "tablist", "aria-label": "Diff view style" },
|
|
402
|
+
React.createElement("button", { type: "button", role: "tab", "aria-selected": diffStyle === 'split', "data-active": diffStyle === 'split', className: "jp-xtralab-DiffWidget-segmentedButton jp-xtralab-DiffWidget-segmentedButton-icon", title: "Split (side-by-side) view", "aria-label": "Split view", onClick: () => onChange('split') },
|
|
403
|
+
React.createElement(DiffSplitIcon, null)),
|
|
404
|
+
React.createElement("button", { type: "button", role: "tab", "aria-selected": diffStyle === 'unified', "data-active": diffStyle === 'unified', className: "jp-xtralab-DiffWidget-segmentedButton jp-xtralab-DiffWidget-segmentedButton-icon", title: "Unified (inline) view", "aria-label": "Unified view", onClick: () => onChange('unified') },
|
|
405
|
+
React.createElement(DiffUnifiedIcon, null))));
|
|
406
|
+
}
|
package/lib/git/diffWidget.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
|
4
4
|
import { Contents } from '@jupyterlab/services';
|
|
5
5
|
import { ReactWidget } from '@jupyterlab/ui-components';
|
|
6
6
|
import { ISignal } from '@lumino/signaling';
|
|
7
|
-
import { DIFF_WIDGET_CSS_CLASS, type NotebookDiffViewMode } from './diffSurface';
|
|
7
|
+
import { DIFF_WIDGET_CSS_CLASS, type DiffStyle, type NotebookDiffViewMode } from './diffSurface';
|
|
8
8
|
import { GitReference, IFileChange } from './tokens';
|
|
9
9
|
export { DIFF_WIDGET_CSS_CLASS };
|
|
10
10
|
export declare const PREVIEW_DIFF_WIDGET_ID = "xtralab:diff:preview";
|
|
@@ -99,6 +99,21 @@ export declare class DiffContentWidget extends ReactWidget {
|
|
|
99
99
|
setHasNotebookView(value: boolean): void;
|
|
100
100
|
/** Emits whenever {@link hasNotebookView} changes. */
|
|
101
101
|
get hasNotebookViewChanged(): ISignal<this, boolean>;
|
|
102
|
+
/** The user's current split-vs-unified layout choice for file diffs. */
|
|
103
|
+
get diffStyle(): DiffStyle;
|
|
104
|
+
/** Update the diff layout and persist it across sessions. */
|
|
105
|
+
setDiffStyle(style: DiffStyle): void;
|
|
106
|
+
/** Emits whenever {@link diffStyle} changes. */
|
|
107
|
+
get diffStyleChanged(): ISignal<this, DiffStyle>;
|
|
108
|
+
/**
|
|
109
|
+
* Whether the textual/code file diff (the only view the split-vs-unified
|
|
110
|
+
* choice affects) is currently active. Set by the React content; the
|
|
111
|
+
* toolbar reads it to decide whether to show the Split/Unified selector.
|
|
112
|
+
*/
|
|
113
|
+
get fileDiffActive(): boolean;
|
|
114
|
+
setFileDiffActive(value: boolean): void;
|
|
115
|
+
/** Emits whenever {@link fileDiffActive} changes. */
|
|
116
|
+
get fileDiffActiveChanged(): ISignal<this, boolean>;
|
|
102
117
|
get pinned(): boolean;
|
|
103
118
|
pin(): void;
|
|
104
119
|
get pinnedChanged(): ISignal<this, boolean>;
|
|
@@ -109,6 +124,10 @@ export declare class DiffContentWidget extends ReactWidget {
|
|
|
109
124
|
private _notebookViewModeChanged;
|
|
110
125
|
private _hasNotebookView;
|
|
111
126
|
private _hasNotebookViewChanged;
|
|
127
|
+
private _diffStyle;
|
|
128
|
+
private _diffStyleChanged;
|
|
129
|
+
private _fileDiffActive;
|
|
130
|
+
private _fileDiffActiveChanged;
|
|
112
131
|
private _pinned;
|
|
113
132
|
private _pinnedChanged;
|
|
114
133
|
}
|
package/lib/git/diffWidget.js
CHANGED
|
@@ -6,7 +6,7 @@ import { Signal } from '@lumino/signaling';
|
|
|
6
6
|
import { getTreeIcon } from '../fileBrowser/icons';
|
|
7
7
|
import { content } from './api';
|
|
8
8
|
import { imageDataType } from './imageDiff';
|
|
9
|
-
import { DIFF_WIDGET_CSS_CLASS, DiffSurface, NotebookViewModeControl, isDarkTheme, readStoredNotebookViewMode, writeStoredNotebookViewMode } from './diffSurface';
|
|
9
|
+
import { DIFF_WIDGET_CSS_CLASS, DiffStyleControl, DiffSurface, NotebookViewModeControl, isDarkTheme, readStoredDiffStyle, readStoredNotebookViewMode, writeStoredDiffStyle, writeStoredNotebookViewMode } from './diffSurface';
|
|
10
10
|
export { DIFF_WIDGET_CSS_CLASS };
|
|
11
11
|
export const PREVIEW_DIFF_WIDGET_ID = 'xtralab:diff:preview';
|
|
12
12
|
export function pinnedDiffWidgetId(change) {
|
|
@@ -101,18 +101,19 @@ export function createGitDiffWidget(options) {
|
|
|
101
101
|
}
|
|
102
102
|
widget.toolbar.addItem('pin', pinButton);
|
|
103
103
|
widget.toolbar.addItem('notebook-view-mode', new NotebookViewModeToolbarItem(content));
|
|
104
|
+
widget.toolbar.addItem('diff-style', new DiffStyleToolbarItem(content));
|
|
104
105
|
const syncToolbarVisibility = () => {
|
|
105
106
|
if (widget === null || widget.isDisposed) {
|
|
106
107
|
return;
|
|
107
108
|
}
|
|
108
|
-
if (!content.pinned || content.hasNotebookView) {
|
|
109
|
+
if (!content.pinned || content.hasNotebookView || content.fileDiffActive) {
|
|
109
110
|
widget.toolbar.show();
|
|
110
111
|
}
|
|
111
112
|
else {
|
|
112
113
|
widget.toolbar.hide();
|
|
113
114
|
}
|
|
114
115
|
};
|
|
115
|
-
const
|
|
116
|
+
const onToolbarStateChanged = () => {
|
|
116
117
|
if (widget === null || widget.isDisposed) {
|
|
117
118
|
return;
|
|
118
119
|
}
|
|
@@ -134,11 +135,13 @@ export function createGitDiffWidget(options) {
|
|
|
134
135
|
}
|
|
135
136
|
syncToolbarVisibility();
|
|
136
137
|
};
|
|
137
|
-
content.hasNotebookViewChanged.connect(
|
|
138
|
+
content.hasNotebookViewChanged.connect(onToolbarStateChanged);
|
|
139
|
+
content.fileDiffActiveChanged.connect(onToolbarStateChanged);
|
|
138
140
|
content.pinnedChanged.connect(onPinnedChanged);
|
|
139
141
|
syncToolbarVisibility();
|
|
140
142
|
widget.disposed.connect(() => {
|
|
141
|
-
content.hasNotebookViewChanged.disconnect(
|
|
143
|
+
content.hasNotebookViewChanged.disconnect(onToolbarStateChanged);
|
|
144
|
+
content.fileDiffActiveChanged.disconnect(onToolbarStateChanged);
|
|
142
145
|
content.pinnedChanged.disconnect(onPinnedChanged);
|
|
143
146
|
});
|
|
144
147
|
return widget;
|
|
@@ -167,10 +170,14 @@ export class DiffContentWidget extends ReactWidget {
|
|
|
167
170
|
this._notebookViewModeChanged = new Signal(this);
|
|
168
171
|
this._hasNotebookView = false;
|
|
169
172
|
this._hasNotebookViewChanged = new Signal(this);
|
|
173
|
+
this._diffStyleChanged = new Signal(this);
|
|
174
|
+
this._fileDiffActive = false;
|
|
175
|
+
this._fileDiffActiveChanged = new Signal(this);
|
|
170
176
|
this._pinnedChanged = new Signal(this);
|
|
171
177
|
this._options = options;
|
|
172
178
|
this._pinned = options.pinned === true;
|
|
173
179
|
this._notebookViewMode = readStoredNotebookViewMode();
|
|
180
|
+
this._diffStyle = readStoredDiffStyle();
|
|
174
181
|
}
|
|
175
182
|
/** The change this widget renders, used by callers to identify the widget. */
|
|
176
183
|
get change() {
|
|
@@ -212,6 +219,42 @@ export class DiffContentWidget extends ReactWidget {
|
|
|
212
219
|
get hasNotebookViewChanged() {
|
|
213
220
|
return this._hasNotebookViewChanged;
|
|
214
221
|
}
|
|
222
|
+
/** The user's current split-vs-unified layout choice for file diffs. */
|
|
223
|
+
get diffStyle() {
|
|
224
|
+
return this._diffStyle;
|
|
225
|
+
}
|
|
226
|
+
/** Update the diff layout and persist it across sessions. */
|
|
227
|
+
setDiffStyle(style) {
|
|
228
|
+
if (style === this._diffStyle) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
this._diffStyle = style;
|
|
232
|
+
writeStoredDiffStyle(style);
|
|
233
|
+
this._diffStyleChanged.emit(style);
|
|
234
|
+
}
|
|
235
|
+
/** Emits whenever {@link diffStyle} changes. */
|
|
236
|
+
get diffStyleChanged() {
|
|
237
|
+
return this._diffStyleChanged;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Whether the textual/code file diff (the only view the split-vs-unified
|
|
241
|
+
* choice affects) is currently active. Set by the React content; the
|
|
242
|
+
* toolbar reads it to decide whether to show the Split/Unified selector.
|
|
243
|
+
*/
|
|
244
|
+
get fileDiffActive() {
|
|
245
|
+
return this._fileDiffActive;
|
|
246
|
+
}
|
|
247
|
+
setFileDiffActive(value) {
|
|
248
|
+
if (value === this._fileDiffActive) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
this._fileDiffActive = value;
|
|
252
|
+
this._fileDiffActiveChanged.emit(value);
|
|
253
|
+
}
|
|
254
|
+
/** Emits whenever {@link fileDiffActive} changes. */
|
|
255
|
+
get fileDiffActiveChanged() {
|
|
256
|
+
return this._fileDiffActiveChanged;
|
|
257
|
+
}
|
|
215
258
|
get pinned() {
|
|
216
259
|
return this._pinned;
|
|
217
260
|
}
|
|
@@ -264,6 +307,25 @@ function DiffViewer(props) {
|
|
|
264
307
|
widget.notebookViewModeChanged.disconnect(handler);
|
|
265
308
|
};
|
|
266
309
|
}, [widget]);
|
|
310
|
+
// Mirror the widget's diff-style choice the same way (the widget owns the
|
|
311
|
+
// canonical value so the toolbar can read and write it without going
|
|
312
|
+
// through React).
|
|
313
|
+
const [diffStyle, setDiffStyleLocal] = React.useState(() => widget.diffStyle);
|
|
314
|
+
React.useEffect(() => {
|
|
315
|
+
const handler = (_sender, style) => {
|
|
316
|
+
setDiffStyleLocal(style);
|
|
317
|
+
};
|
|
318
|
+
widget.diffStyleChanged.connect(handler);
|
|
319
|
+
setDiffStyleLocal(widget.diffStyle);
|
|
320
|
+
return () => {
|
|
321
|
+
widget.diffStyleChanged.disconnect(handler);
|
|
322
|
+
};
|
|
323
|
+
}, [widget]);
|
|
324
|
+
// Tell the host widget whether the textual/code file diff is the active
|
|
325
|
+
// view, so the toolbar's Split/Unified selector only shows when it applies.
|
|
326
|
+
const handleFileDiffActiveChange = React.useCallback((active) => {
|
|
327
|
+
widget.setFileDiffActive(active);
|
|
328
|
+
}, [widget]);
|
|
267
329
|
// Tell the host widget whether a rendered notebook view is currently
|
|
268
330
|
// available. The toolbar item that lives in the `MainAreaWidget`
|
|
269
331
|
// toolbar reads this flag to decide whether to show the
|
|
@@ -408,7 +470,7 @@ function DiffViewer(props) {
|
|
|
408
470
|
setReloadKey(key => key + 1);
|
|
409
471
|
}
|
|
410
472
|
}), [canDiscardHunk, contentsManager, onChanged, state.serverPath]);
|
|
411
|
-
return (React.createElement(DiffSurface, { loading: state.loading, error: state.error, isBinary: state.isBinary, oldText: state.oldText, newText: state.newText, newName: change.path, oldName: (_a = change.from) !== null && _a !== void 0 ? _a : change.path, dark: dark, rendermime: rendermime, notebookViewMode: notebookViewMode, onNotebookAvailabilityChange: handleNotebookAvailabilityChange, onMetadataChange: handleMetadataChange, hunkDiscard: hunkDiscard }));
|
|
473
|
+
return (React.createElement(DiffSurface, { loading: state.loading, error: state.error, isBinary: state.isBinary, oldText: state.oldText, newText: state.newText, newName: change.path, oldName: (_a = change.from) !== null && _a !== void 0 ? _a : change.path, dark: dark, rendermime: rendermime, notebookViewMode: notebookViewMode, diffStyle: diffStyle, onNotebookAvailabilityChange: handleNotebookAvailabilityChange, onFileDiffActiveChange: handleFileDiffActiveChange, onMetadataChange: handleMetadataChange, hunkDiscard: hunkDiscard }));
|
|
412
474
|
}
|
|
413
475
|
/**
|
|
414
476
|
* Segmented Notebook/JSON selector mounted into the `MainAreaWidget`
|
|
@@ -451,6 +513,45 @@ function NotebookViewModeToolbarControl(props) {
|
|
|
451
513
|
}, [content]);
|
|
452
514
|
return (React.createElement(NotebookViewModeControl, { mode: mode, available: available, onChange: next => content.setNotebookViewMode(next) }));
|
|
453
515
|
}
|
|
516
|
+
/**
|
|
517
|
+
* Split/Unified selector mounted into the `MainAreaWidget` toolbar. Mirrors
|
|
518
|
+
* {@link NotebookViewModeToolbarItem}: it reads from and writes to the
|
|
519
|
+
* {@link DiffContentWidget} so toolbar interactions and React state stay in
|
|
520
|
+
* sync, and only appears while the textual/code file diff is the active view
|
|
521
|
+
* (driven by the same `fileDiffActive` signal that gates toolbar visibility).
|
|
522
|
+
*/
|
|
523
|
+
class DiffStyleToolbarItem extends ReactWidget {
|
|
524
|
+
constructor(content) {
|
|
525
|
+
super();
|
|
526
|
+
this._content = content;
|
|
527
|
+
this.addClass('jp-xtralab-DiffWidget-diffStyleToolbarItem');
|
|
528
|
+
}
|
|
529
|
+
render() {
|
|
530
|
+
return React.createElement(DiffStyleToolbarControl, { content: this._content });
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
function DiffStyleToolbarControl(props) {
|
|
534
|
+
const { content } = props;
|
|
535
|
+
const [style, setStyle] = React.useState(() => content.diffStyle);
|
|
536
|
+
const [available, setAvailable] = React.useState(() => content.fileDiffActive);
|
|
537
|
+
React.useEffect(() => {
|
|
538
|
+
const onStyle = (_sender, next) => {
|
|
539
|
+
setStyle(next);
|
|
540
|
+
};
|
|
541
|
+
const onAvailable = (_sender, next) => {
|
|
542
|
+
setAvailable(next);
|
|
543
|
+
};
|
|
544
|
+
content.diffStyleChanged.connect(onStyle);
|
|
545
|
+
content.fileDiffActiveChanged.connect(onAvailable);
|
|
546
|
+
setStyle(content.diffStyle);
|
|
547
|
+
setAvailable(content.fileDiffActive);
|
|
548
|
+
return () => {
|
|
549
|
+
content.diffStyleChanged.disconnect(onStyle);
|
|
550
|
+
content.fileDiffActiveChanged.disconnect(onAvailable);
|
|
551
|
+
};
|
|
552
|
+
}, [content]);
|
|
553
|
+
return (React.createElement(DiffStyleControl, { diffStyle: style, available: available, onChange: next => content.setDiffStyle(next) }));
|
|
554
|
+
}
|
|
454
555
|
function formatTitle(change) {
|
|
455
556
|
var _a;
|
|
456
557
|
const name = (_a = change.path.split('/').pop()) !== null && _a !== void 0 ? _a : change.path;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xtralab",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "An opinionated JupyterLab meta-package that bundles a curated set of extensions, ships a path-first file browser, renders jupyterlab-git's text, notebook and image diffs with its own viewers, and applies a quieter default workspace configuration.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
package/src/git/diffProvider.tsx
CHANGED
|
@@ -15,11 +15,15 @@ import { ISignal, Signal } from '@lumino/signaling';
|
|
|
15
15
|
|
|
16
16
|
import {
|
|
17
17
|
DIFF_WIDGET_CSS_CLASS,
|
|
18
|
+
DiffStyleControl,
|
|
18
19
|
DiffSurface,
|
|
19
20
|
NotebookViewModeControl,
|
|
20
21
|
isDarkTheme,
|
|
22
|
+
readStoredDiffStyle,
|
|
21
23
|
readStoredNotebookViewMode,
|
|
24
|
+
writeStoredDiffStyle,
|
|
22
25
|
writeStoredNotebookViewMode,
|
|
26
|
+
type DiffStyle,
|
|
23
27
|
type NotebookDiffViewMode
|
|
24
28
|
} from './diffSurface';
|
|
25
29
|
import { IMAGE_DIFF_EXTENSIONS, imageDataType } from './imageDiff';
|
|
@@ -60,6 +64,7 @@ class XtralabDiffWidget extends ReactWidget implements Git.Diff.IDiffWidget {
|
|
|
60
64
|
this._model = model;
|
|
61
65
|
this._context = context;
|
|
62
66
|
this._notebookViewMode = readStoredNotebookViewMode();
|
|
67
|
+
this._diffStyle = readStoredDiffStyle();
|
|
63
68
|
// `jp-git-diff-root` borrows jupyterlab-git's host sizing (height:100%
|
|
64
69
|
// flex column inside its PreviewMainAreaWidget); `jp-xtralab-DiffWidget`
|
|
65
70
|
// brings our diff theming + the `.jp-xtralab-DiffWidget-*` styles.
|
|
@@ -146,6 +151,45 @@ class XtralabDiffWidget extends ReactWidget implements Git.Diff.IDiffWidget {
|
|
|
146
151
|
return this._hasNotebookViewChanged;
|
|
147
152
|
}
|
|
148
153
|
|
|
154
|
+
/** The user's current split-vs-unified layout choice for file diffs. */
|
|
155
|
+
get diffStyle(): DiffStyle {
|
|
156
|
+
return this._diffStyle;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
setDiffStyle(style: DiffStyle): void {
|
|
160
|
+
if (style === this._diffStyle) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
this._diffStyle = style;
|
|
164
|
+
writeStoredDiffStyle(style);
|
|
165
|
+
this._diffStyleChanged.emit(style);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
get diffStyleChanged(): ISignal<this, DiffStyle> {
|
|
169
|
+
return this._diffStyleChanged;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Whether the textual/code file diff (the only view the split-vs-unified
|
|
174
|
+
* choice affects) is currently active; mirrored into the toolbar so the
|
|
175
|
+
* Split/Unified selector only shows when it applies.
|
|
176
|
+
*/
|
|
177
|
+
get fileDiffActive(): boolean {
|
|
178
|
+
return this._fileDiffActive;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
setFileDiffActive(value: boolean): void {
|
|
182
|
+
if (value === this._fileDiffActive) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
this._fileDiffActive = value;
|
|
186
|
+
this._fileDiffActiveChanged.emit(value);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
get fileDiffActiveChanged(): ISignal<this, boolean> {
|
|
190
|
+
return this._fileDiffActiveChanged;
|
|
191
|
+
}
|
|
192
|
+
|
|
149
193
|
/** Resolve the in-flight {@link refresh} promise, if any. */
|
|
150
194
|
settleRefresh(): void {
|
|
151
195
|
const pending = this._pendingRefresh;
|
|
@@ -175,6 +219,10 @@ class XtralabDiffWidget extends ReactWidget implements Git.Diff.IDiffWidget {
|
|
|
175
219
|
);
|
|
176
220
|
private _hasNotebookView = false;
|
|
177
221
|
private _hasNotebookViewChanged = new Signal<this, boolean>(this);
|
|
222
|
+
private _diffStyle: DiffStyle;
|
|
223
|
+
private _diffStyleChanged = new Signal<this, DiffStyle>(this);
|
|
224
|
+
private _fileDiffActive = false;
|
|
225
|
+
private _fileDiffActiveChanged = new Signal<this, boolean>(this);
|
|
178
226
|
}
|
|
179
227
|
|
|
180
228
|
interface IModelDiffState {
|
|
@@ -225,6 +273,29 @@ function ModelDiffView(props: {
|
|
|
225
273
|
[widget]
|
|
226
274
|
);
|
|
227
275
|
|
|
276
|
+
// Mirror the widget's diff-style choice so toolbar-driven changes
|
|
277
|
+
// re-render the diff. The widget owns the canonical value.
|
|
278
|
+
const [diffStyle, setDiffStyle] = React.useState<DiffStyle>(
|
|
279
|
+
() => widget.diffStyle
|
|
280
|
+
);
|
|
281
|
+
React.useEffect(() => {
|
|
282
|
+
const handler = (_sender: XtralabDiffWidget, style: DiffStyle): void => {
|
|
283
|
+
setDiffStyle(style);
|
|
284
|
+
};
|
|
285
|
+
widget.diffStyleChanged.connect(handler);
|
|
286
|
+
setDiffStyle(widget.diffStyle);
|
|
287
|
+
return () => {
|
|
288
|
+
widget.diffStyleChanged.disconnect(handler);
|
|
289
|
+
};
|
|
290
|
+
}, [widget]);
|
|
291
|
+
|
|
292
|
+
const handleFileDiffActiveChange = React.useCallback(
|
|
293
|
+
(active: boolean) => {
|
|
294
|
+
widget.setFileDiffActive(active);
|
|
295
|
+
},
|
|
296
|
+
[widget]
|
|
297
|
+
);
|
|
298
|
+
|
|
228
299
|
const [dark, setDark] = React.useState<boolean>(() =>
|
|
229
300
|
isDarkTheme(themeManager)
|
|
230
301
|
);
|
|
@@ -337,7 +408,9 @@ function ModelDiffView(props: {
|
|
|
337
408
|
dark={dark}
|
|
338
409
|
rendermime={rendermime}
|
|
339
410
|
notebookViewMode={notebookViewMode}
|
|
411
|
+
diffStyle={diffStyle}
|
|
340
412
|
onNotebookAvailabilityChange={handleNotebookAvailabilityChange}
|
|
413
|
+
onFileDiffActiveChange={handleFileDiffActiveChange}
|
|
341
414
|
hunkDiscard={hunkDiscard}
|
|
342
415
|
/>
|
|
343
416
|
);
|
|
@@ -401,6 +474,60 @@ function ToolbarControl(props: {
|
|
|
401
474
|
);
|
|
402
475
|
}
|
|
403
476
|
|
|
477
|
+
/**
|
|
478
|
+
* Split/Unified toggle mounted into the diff toolbar jupyterlab-git passes
|
|
479
|
+
* to the factory. Bound to the widget's signals so it stays in sync with the
|
|
480
|
+
* rendered surface and only appears while the textual/code file diff is the
|
|
481
|
+
* active view.
|
|
482
|
+
*/
|
|
483
|
+
class DiffStyleToolbarItem extends ReactWidget {
|
|
484
|
+
constructor(widget: XtralabDiffWidget) {
|
|
485
|
+
super();
|
|
486
|
+
this._widget = widget;
|
|
487
|
+
this.addClass('jp-xtralab-DiffWidget-diffStyleToolbarItem');
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
protected render(): React.ReactElement {
|
|
491
|
+
return <DiffStyleControlBound widget={this._widget} />;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private _widget: XtralabDiffWidget;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function DiffStyleControlBound(props: {
|
|
498
|
+
widget: XtralabDiffWidget;
|
|
499
|
+
}): React.ReactElement {
|
|
500
|
+
const { widget } = props;
|
|
501
|
+
const [style, setStyle] = React.useState<DiffStyle>(() => widget.diffStyle);
|
|
502
|
+
const [available, setAvailable] = React.useState<boolean>(
|
|
503
|
+
() => widget.fileDiffActive
|
|
504
|
+
);
|
|
505
|
+
React.useEffect(() => {
|
|
506
|
+
const onStyle = (_sender: XtralabDiffWidget, next: DiffStyle): void => {
|
|
507
|
+
setStyle(next);
|
|
508
|
+
};
|
|
509
|
+
const onAvailable = (_sender: XtralabDiffWidget, next: boolean): void => {
|
|
510
|
+
setAvailable(next);
|
|
511
|
+
};
|
|
512
|
+
widget.diffStyleChanged.connect(onStyle);
|
|
513
|
+
widget.fileDiffActiveChanged.connect(onAvailable);
|
|
514
|
+
setStyle(widget.diffStyle);
|
|
515
|
+
setAvailable(widget.fileDiffActive);
|
|
516
|
+
return () => {
|
|
517
|
+
widget.diffStyleChanged.disconnect(onStyle);
|
|
518
|
+
widget.fileDiffActiveChanged.disconnect(onAvailable);
|
|
519
|
+
};
|
|
520
|
+
}, [widget]);
|
|
521
|
+
|
|
522
|
+
return (
|
|
523
|
+
<DiffStyleControl
|
|
524
|
+
diffStyle={style}
|
|
525
|
+
available={available}
|
|
526
|
+
onChange={next => widget.setDiffStyle(next)}
|
|
527
|
+
/>
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
|
|
404
531
|
/**
|
|
405
532
|
* Build the `Git.Diff.Factory` xtralab registers with jupyterlab-git. The
|
|
406
533
|
* factory closes over the application-level context (contents manager for
|
|
@@ -417,6 +544,10 @@ function makeXtralabDiffFactory(
|
|
|
417
544
|
'xtralab-notebook-view-mode',
|
|
418
545
|
new NotebookViewModeToolbarItem(widget)
|
|
419
546
|
);
|
|
547
|
+
options.toolbar.addItem(
|
|
548
|
+
'xtralab-diff-style',
|
|
549
|
+
new DiffStyleToolbarItem(widget)
|
|
550
|
+
);
|
|
420
551
|
}
|
|
421
552
|
return widget;
|
|
422
553
|
};
|
package/src/git/diffSurface.tsx
CHANGED
|
@@ -101,6 +101,35 @@ function writeStoredSplitRatio(ratio: number): void {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Layout for textual/code diffs, forwarded to `@pierre/diffs` as its
|
|
106
|
+
* `diffStyle` option. `split` is the side-by-side old|new view; `unified`
|
|
107
|
+
* is the single-column inline view (what diffs.com calls "stacked").
|
|
108
|
+
*/
|
|
109
|
+
export type DiffStyle = 'split' | 'unified';
|
|
110
|
+
|
|
111
|
+
const DIFF_STYLE_STORAGE_KEY = 'xtralab:diff-style';
|
|
112
|
+
|
|
113
|
+
export function readStoredDiffStyle(): DiffStyle {
|
|
114
|
+
try {
|
|
115
|
+
const raw = window.localStorage.getItem(DIFF_STYLE_STORAGE_KEY);
|
|
116
|
+
if (raw === 'split' || raw === 'unified') {
|
|
117
|
+
return raw;
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
// See readStoredSplitRatio — privacy-mode / sandboxed contexts can throw.
|
|
121
|
+
}
|
|
122
|
+
return 'split';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function writeStoredDiffStyle(style: DiffStyle): void {
|
|
126
|
+
try {
|
|
127
|
+
window.localStorage.setItem(DIFF_STYLE_STORAGE_KEY, style);
|
|
128
|
+
} catch {
|
|
129
|
+
// Best-effort.
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
104
133
|
/**
|
|
105
134
|
* View modes available for `.ipynb` diffs. `notebook` is the cell-by-cell
|
|
106
135
|
* rendered view; `json` is the raw nbformat JSON file diff, for inspecting
|
|
@@ -212,11 +241,20 @@ export interface IDiffSurfaceProps {
|
|
|
212
241
|
rendermime: IRenderMimeRegistry | null;
|
|
213
242
|
/** Current rendered-vs-JSON choice for notebook diffs (host-controlled). */
|
|
214
243
|
notebookViewMode: NotebookDiffViewMode;
|
|
244
|
+
/** Split vs unified layout for the textual/code file diff (host-controlled). */
|
|
245
|
+
diffStyle: DiffStyle;
|
|
215
246
|
/**
|
|
216
247
|
* Called whenever the availability of a rendered notebook view changes,
|
|
217
248
|
* so the host can show/hide its Notebook/JSON toolbar toggle.
|
|
218
249
|
*/
|
|
219
250
|
onNotebookAvailabilityChange?: (available: boolean) => void;
|
|
251
|
+
/**
|
|
252
|
+
* Called whenever the textual/code file diff (the only view the split vs
|
|
253
|
+
* unified choice affects) becomes the active view, so the host can
|
|
254
|
+
* show/hide its Split/Unified toolbar toggle. False for image, binary,
|
|
255
|
+
* rendered-notebook, loading and error states.
|
|
256
|
+
*/
|
|
257
|
+
onFileDiffActiveChange?: (active: boolean) => void;
|
|
220
258
|
/**
|
|
221
259
|
* Called after each (re)computation of the diff with the number of hunks
|
|
222
260
|
* (or `null` when no textual diff exists). Hosts use this to implement
|
|
@@ -243,7 +281,9 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
|
|
|
243
281
|
dark,
|
|
244
282
|
rendermime,
|
|
245
283
|
notebookViewMode,
|
|
284
|
+
diffStyle,
|
|
246
285
|
onNotebookAvailabilityChange,
|
|
286
|
+
onFileDiffActiveChange,
|
|
247
287
|
onMetadataChange,
|
|
248
288
|
hunkDiscard
|
|
249
289
|
} = props;
|
|
@@ -295,6 +335,21 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
|
|
|
295
335
|
});
|
|
296
336
|
}, [onMetadataChange, metadata]);
|
|
297
337
|
|
|
338
|
+
// Decide which view to render. The notebook view requires a successful
|
|
339
|
+
// nbformat parse; if that failed, we only show the raw JSON file diff.
|
|
340
|
+
// Computed before the early returns below so the file-diff-active effect
|
|
341
|
+
// can depend on it without violating the rules of hooks.
|
|
342
|
+
const hasNotebookView = notebookDiff !== null;
|
|
343
|
+
const showNotebookView = hasNotebookView && notebookViewMode === 'notebook';
|
|
344
|
+
const showFileDiff = !showNotebookView && metadata !== null;
|
|
345
|
+
|
|
346
|
+
// The split vs unified choice only affects the textual/code file diff, so
|
|
347
|
+
// tell the host whether that view is active to drive its Split/Unified
|
|
348
|
+
// toolbar toggle.
|
|
349
|
+
React.useEffect(() => {
|
|
350
|
+
onFileDiffActiveChange?.(showFileDiff);
|
|
351
|
+
}, [onFileDiffActiveChange, showFileDiff]);
|
|
352
|
+
|
|
298
353
|
// Split ratio for the diff columns: fraction of width given to the
|
|
299
354
|
// deletions (left) pane. Persisted across sessions so the user only
|
|
300
355
|
// dials in their layout once.
|
|
@@ -461,12 +516,6 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
|
|
|
461
516
|
</div>
|
|
462
517
|
);
|
|
463
518
|
}
|
|
464
|
-
// Decide which view to render. The notebook view requires a successful
|
|
465
|
-
// nbformat parse; if that failed, we only show the raw JSON file diff.
|
|
466
|
-
const hasNotebookView = notebookDiff !== null;
|
|
467
|
-
const showNotebookView = hasNotebookView && notebookViewMode === 'notebook';
|
|
468
|
-
const showFileDiff = !showNotebookView && metadata !== null;
|
|
469
|
-
|
|
470
519
|
if (!showNotebookView && !showFileDiff) {
|
|
471
520
|
return (
|
|
472
521
|
<div className="jp-xtralab-DiffWidget-status">No content to diff.</div>
|
|
@@ -506,7 +555,7 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
|
|
|
506
555
|
// fine for the small files git diffs typically operate on.
|
|
507
556
|
disableWorkerPool={true}
|
|
508
557
|
options={{
|
|
509
|
-
diffStyle
|
|
558
|
+
diffStyle,
|
|
510
559
|
// Drop the file header — the tab title already shows the
|
|
511
560
|
// file name and the panel header carries the change
|
|
512
561
|
// context.
|
|
@@ -523,7 +572,7 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
|
|
|
523
572
|
/>
|
|
524
573
|
) : null}
|
|
525
574
|
</div>
|
|
526
|
-
{showFileDiff ? (
|
|
575
|
+
{showFileDiff && diffStyle === 'split' ? (
|
|
527
576
|
<div
|
|
528
577
|
className="jp-xtralab-DiffWidget-resizer"
|
|
529
578
|
style={{ left: `${leftPercent}%` }}
|
|
@@ -586,3 +635,90 @@ export function NotebookViewModeControl(props: {
|
|
|
586
635
|
</div>
|
|
587
636
|
);
|
|
588
637
|
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Split / unified glyphs, inlined from the `@pierre/diffs` icon sprite
|
|
641
|
+
* (`diffs-icon-diff-split` / `diffs-icon-diff-unified`) so the toggle looks
|
|
642
|
+
* like the one on the library's own docs site without depending on the
|
|
643
|
+
* sprite sheet being injected into the document. `currentColor` lets the
|
|
644
|
+
* segmented-button styling drive the fill (including the active state).
|
|
645
|
+
*/
|
|
646
|
+
function DiffSplitIcon(): React.ReactElement {
|
|
647
|
+
return (
|
|
648
|
+
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
649
|
+
<path d="M14 0H8.5v16H14a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2m-1.5 6.5v1h1a.5.5 0 0 1 0 1h-1v1a.5.5 0 0 1-1 0v-1h-1a.5.5 0 0 1 0-1h1v-1a.5.5 0 0 1 1 0" />
|
|
650
|
+
<path
|
|
651
|
+
d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h5.5V0zm.5 7.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1"
|
|
652
|
+
opacity=".3"
|
|
653
|
+
/>
|
|
654
|
+
</svg>
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function DiffUnifiedIcon(): React.ReactElement {
|
|
659
|
+
return (
|
|
660
|
+
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
661
|
+
<path
|
|
662
|
+
fillRule="evenodd"
|
|
663
|
+
d="M16 14a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V8.5h16zm-8-4a.5.5 0 0 0-.5.5v1h-1a.5.5 0 0 0 0 1h1v1a.5.5 0 0 0 1 0v-1h1a.5.5 0 0 0 0-1h-1v-1A.5.5 0 0 0 8 10"
|
|
664
|
+
clipRule="evenodd"
|
|
665
|
+
/>
|
|
666
|
+
<path
|
|
667
|
+
fillRule="evenodd"
|
|
668
|
+
d="M14 0a2 2 0 0 1 2 2v5.5H0V2a2 2 0 0 1 2-2zM6.5 3.5a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1z"
|
|
669
|
+
clipRule="evenodd"
|
|
670
|
+
opacity=".4"
|
|
671
|
+
/>
|
|
672
|
+
</svg>
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Segmented Split/Unified selector. Mirrors {@link NotebookViewModeControl}:
|
|
678
|
+
* hosts mount it into their own toolbar and drive its value/visibility from
|
|
679
|
+
* the same state they pass to {@link DiffSurface}. Only meaningful while the
|
|
680
|
+
* textual/code file diff is the active view, so `available` mirrors the
|
|
681
|
+
* surface's `onFileDiffActiveChange`.
|
|
682
|
+
*/
|
|
683
|
+
export function DiffStyleControl(props: {
|
|
684
|
+
diffStyle: DiffStyle;
|
|
685
|
+
available: boolean;
|
|
686
|
+
onChange: (style: DiffStyle) => void;
|
|
687
|
+
}): React.ReactElement {
|
|
688
|
+
const { diffStyle, available, onChange } = props;
|
|
689
|
+
if (!available) {
|
|
690
|
+
return <></>;
|
|
691
|
+
}
|
|
692
|
+
return (
|
|
693
|
+
<div
|
|
694
|
+
className="jp-xtralab-DiffWidget-segmented"
|
|
695
|
+
role="tablist"
|
|
696
|
+
aria-label="Diff view style"
|
|
697
|
+
>
|
|
698
|
+
<button
|
|
699
|
+
type="button"
|
|
700
|
+
role="tab"
|
|
701
|
+
aria-selected={diffStyle === 'split'}
|
|
702
|
+
data-active={diffStyle === 'split'}
|
|
703
|
+
className="jp-xtralab-DiffWidget-segmentedButton jp-xtralab-DiffWidget-segmentedButton-icon"
|
|
704
|
+
title="Split (side-by-side) view"
|
|
705
|
+
aria-label="Split view"
|
|
706
|
+
onClick={() => onChange('split')}
|
|
707
|
+
>
|
|
708
|
+
<DiffSplitIcon />
|
|
709
|
+
</button>
|
|
710
|
+
<button
|
|
711
|
+
type="button"
|
|
712
|
+
role="tab"
|
|
713
|
+
aria-selected={diffStyle === 'unified'}
|
|
714
|
+
data-active={diffStyle === 'unified'}
|
|
715
|
+
className="jp-xtralab-DiffWidget-segmentedButton jp-xtralab-DiffWidget-segmentedButton-icon"
|
|
716
|
+
title="Unified (inline) view"
|
|
717
|
+
aria-label="Unified view"
|
|
718
|
+
onClick={() => onChange('unified')}
|
|
719
|
+
>
|
|
720
|
+
<DiffUnifiedIcon />
|
|
721
|
+
</button>
|
|
722
|
+
</div>
|
|
723
|
+
);
|
|
724
|
+
}
|
package/src/git/diffWidget.tsx
CHANGED
|
@@ -16,11 +16,15 @@ import { content } from './api';
|
|
|
16
16
|
import { imageDataType } from './imageDiff';
|
|
17
17
|
import {
|
|
18
18
|
DIFF_WIDGET_CSS_CLASS,
|
|
19
|
+
DiffStyleControl,
|
|
19
20
|
DiffSurface,
|
|
20
21
|
NotebookViewModeControl,
|
|
21
22
|
isDarkTheme,
|
|
23
|
+
readStoredDiffStyle,
|
|
22
24
|
readStoredNotebookViewMode,
|
|
25
|
+
writeStoredDiffStyle,
|
|
23
26
|
writeStoredNotebookViewMode,
|
|
27
|
+
type DiffStyle,
|
|
24
28
|
type NotebookDiffViewMode
|
|
25
29
|
} from './diffSurface';
|
|
26
30
|
import { GitReference, IFileChange } from './tokens';
|
|
@@ -158,17 +162,18 @@ export function createGitDiffWidget(
|
|
|
158
162
|
'notebook-view-mode',
|
|
159
163
|
new NotebookViewModeToolbarItem(content)
|
|
160
164
|
);
|
|
165
|
+
widget.toolbar.addItem('diff-style', new DiffStyleToolbarItem(content));
|
|
161
166
|
const syncToolbarVisibility = (): void => {
|
|
162
167
|
if (widget === null || widget.isDisposed) {
|
|
163
168
|
return;
|
|
164
169
|
}
|
|
165
|
-
if (!content.pinned || content.hasNotebookView) {
|
|
170
|
+
if (!content.pinned || content.hasNotebookView || content.fileDiffActive) {
|
|
166
171
|
widget.toolbar.show();
|
|
167
172
|
} else {
|
|
168
173
|
widget.toolbar.hide();
|
|
169
174
|
}
|
|
170
175
|
};
|
|
171
|
-
const
|
|
176
|
+
const onToolbarStateChanged = (): void => {
|
|
172
177
|
if (widget === null || widget.isDisposed) {
|
|
173
178
|
return;
|
|
174
179
|
}
|
|
@@ -190,11 +195,13 @@ export function createGitDiffWidget(
|
|
|
190
195
|
}
|
|
191
196
|
syncToolbarVisibility();
|
|
192
197
|
};
|
|
193
|
-
content.hasNotebookViewChanged.connect(
|
|
198
|
+
content.hasNotebookViewChanged.connect(onToolbarStateChanged);
|
|
199
|
+
content.fileDiffActiveChanged.connect(onToolbarStateChanged);
|
|
194
200
|
content.pinnedChanged.connect(onPinnedChanged);
|
|
195
201
|
syncToolbarVisibility();
|
|
196
202
|
widget.disposed.connect(() => {
|
|
197
|
-
content.hasNotebookViewChanged.disconnect(
|
|
203
|
+
content.hasNotebookViewChanged.disconnect(onToolbarStateChanged);
|
|
204
|
+
content.fileDiffActiveChanged.disconnect(onToolbarStateChanged);
|
|
198
205
|
content.pinnedChanged.disconnect(onPinnedChanged);
|
|
199
206
|
});
|
|
200
207
|
return widget;
|
|
@@ -228,6 +235,7 @@ export class DiffContentWidget extends ReactWidget {
|
|
|
228
235
|
this._options = options;
|
|
229
236
|
this._pinned = options.pinned === true;
|
|
230
237
|
this._notebookViewMode = readStoredNotebookViewMode();
|
|
238
|
+
this._diffStyle = readStoredDiffStyle();
|
|
231
239
|
}
|
|
232
240
|
|
|
233
241
|
/** The change this widget renders, used by callers to identify the widget. */
|
|
@@ -277,6 +285,48 @@ export class DiffContentWidget extends ReactWidget {
|
|
|
277
285
|
return this._hasNotebookViewChanged;
|
|
278
286
|
}
|
|
279
287
|
|
|
288
|
+
/** The user's current split-vs-unified layout choice for file diffs. */
|
|
289
|
+
get diffStyle(): DiffStyle {
|
|
290
|
+
return this._diffStyle;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Update the diff layout and persist it across sessions. */
|
|
294
|
+
setDiffStyle(style: DiffStyle): void {
|
|
295
|
+
if (style === this._diffStyle) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
this._diffStyle = style;
|
|
299
|
+
writeStoredDiffStyle(style);
|
|
300
|
+
this._diffStyleChanged.emit(style);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Emits whenever {@link diffStyle} changes. */
|
|
304
|
+
get diffStyleChanged(): ISignal<this, DiffStyle> {
|
|
305
|
+
return this._diffStyleChanged;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Whether the textual/code file diff (the only view the split-vs-unified
|
|
310
|
+
* choice affects) is currently active. Set by the React content; the
|
|
311
|
+
* toolbar reads it to decide whether to show the Split/Unified selector.
|
|
312
|
+
*/
|
|
313
|
+
get fileDiffActive(): boolean {
|
|
314
|
+
return this._fileDiffActive;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
setFileDiffActive(value: boolean): void {
|
|
318
|
+
if (value === this._fileDiffActive) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
this._fileDiffActive = value;
|
|
322
|
+
this._fileDiffActiveChanged.emit(value);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Emits whenever {@link fileDiffActive} changes. */
|
|
326
|
+
get fileDiffActiveChanged(): ISignal<this, boolean> {
|
|
327
|
+
return this._fileDiffActiveChanged;
|
|
328
|
+
}
|
|
329
|
+
|
|
280
330
|
get pinned(): boolean {
|
|
281
331
|
return this._pinned;
|
|
282
332
|
}
|
|
@@ -309,6 +359,10 @@ export class DiffContentWidget extends ReactWidget {
|
|
|
309
359
|
);
|
|
310
360
|
private _hasNotebookView = false;
|
|
311
361
|
private _hasNotebookViewChanged = new Signal<this, boolean>(this);
|
|
362
|
+
private _diffStyle: DiffStyle;
|
|
363
|
+
private _diffStyleChanged = new Signal<this, DiffStyle>(this);
|
|
364
|
+
private _fileDiffActive = false;
|
|
365
|
+
private _fileDiffActiveChanged = new Signal<this, boolean>(this);
|
|
312
366
|
private _pinned: boolean;
|
|
313
367
|
private _pinnedChanged = new Signal<this, boolean>(this);
|
|
314
368
|
}
|
|
@@ -379,6 +433,32 @@ function DiffViewer(
|
|
|
379
433
|
};
|
|
380
434
|
}, [widget]);
|
|
381
435
|
|
|
436
|
+
// Mirror the widget's diff-style choice the same way (the widget owns the
|
|
437
|
+
// canonical value so the toolbar can read and write it without going
|
|
438
|
+
// through React).
|
|
439
|
+
const [diffStyle, setDiffStyleLocal] = React.useState<DiffStyle>(
|
|
440
|
+
() => widget.diffStyle
|
|
441
|
+
);
|
|
442
|
+
React.useEffect(() => {
|
|
443
|
+
const handler = (_sender: DiffContentWidget, style: DiffStyle): void => {
|
|
444
|
+
setDiffStyleLocal(style);
|
|
445
|
+
};
|
|
446
|
+
widget.diffStyleChanged.connect(handler);
|
|
447
|
+
setDiffStyleLocal(widget.diffStyle);
|
|
448
|
+
return () => {
|
|
449
|
+
widget.diffStyleChanged.disconnect(handler);
|
|
450
|
+
};
|
|
451
|
+
}, [widget]);
|
|
452
|
+
|
|
453
|
+
// Tell the host widget whether the textual/code file diff is the active
|
|
454
|
+
// view, so the toolbar's Split/Unified selector only shows when it applies.
|
|
455
|
+
const handleFileDiffActiveChange = React.useCallback(
|
|
456
|
+
(active: boolean) => {
|
|
457
|
+
widget.setFileDiffActive(active);
|
|
458
|
+
},
|
|
459
|
+
[widget]
|
|
460
|
+
);
|
|
461
|
+
|
|
382
462
|
// Tell the host widget whether a rendered notebook view is currently
|
|
383
463
|
// available. The toolbar item that lives in the `MainAreaWidget`
|
|
384
464
|
// toolbar reads this flag to decide whether to show the
|
|
@@ -558,7 +638,9 @@ function DiffViewer(
|
|
|
558
638
|
dark={dark}
|
|
559
639
|
rendermime={rendermime}
|
|
560
640
|
notebookViewMode={notebookViewMode}
|
|
641
|
+
diffStyle={diffStyle}
|
|
561
642
|
onNotebookAvailabilityChange={handleNotebookAvailabilityChange}
|
|
643
|
+
onFileDiffActiveChange={handleFileDiffActiveChange}
|
|
562
644
|
onMetadataChange={handleMetadataChange}
|
|
563
645
|
hunkDiscard={hunkDiscard}
|
|
564
646
|
/>
|
|
@@ -627,6 +709,61 @@ function NotebookViewModeToolbarControl(props: {
|
|
|
627
709
|
);
|
|
628
710
|
}
|
|
629
711
|
|
|
712
|
+
/**
|
|
713
|
+
* Split/Unified selector mounted into the `MainAreaWidget` toolbar. Mirrors
|
|
714
|
+
* {@link NotebookViewModeToolbarItem}: it reads from and writes to the
|
|
715
|
+
* {@link DiffContentWidget} so toolbar interactions and React state stay in
|
|
716
|
+
* sync, and only appears while the textual/code file diff is the active view
|
|
717
|
+
* (driven by the same `fileDiffActive` signal that gates toolbar visibility).
|
|
718
|
+
*/
|
|
719
|
+
class DiffStyleToolbarItem extends ReactWidget {
|
|
720
|
+
constructor(content: DiffContentWidget) {
|
|
721
|
+
super();
|
|
722
|
+
this._content = content;
|
|
723
|
+
this.addClass('jp-xtralab-DiffWidget-diffStyleToolbarItem');
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
protected render(): React.ReactElement {
|
|
727
|
+
return <DiffStyleToolbarControl content={this._content} />;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
private _content: DiffContentWidget;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function DiffStyleToolbarControl(props: {
|
|
734
|
+
content: DiffContentWidget;
|
|
735
|
+
}): React.ReactElement {
|
|
736
|
+
const { content } = props;
|
|
737
|
+
const [style, setStyle] = React.useState<DiffStyle>(() => content.diffStyle);
|
|
738
|
+
const [available, setAvailable] = React.useState<boolean>(
|
|
739
|
+
() => content.fileDiffActive
|
|
740
|
+
);
|
|
741
|
+
React.useEffect(() => {
|
|
742
|
+
const onStyle = (_sender: DiffContentWidget, next: DiffStyle): void => {
|
|
743
|
+
setStyle(next);
|
|
744
|
+
};
|
|
745
|
+
const onAvailable = (_sender: DiffContentWidget, next: boolean): void => {
|
|
746
|
+
setAvailable(next);
|
|
747
|
+
};
|
|
748
|
+
content.diffStyleChanged.connect(onStyle);
|
|
749
|
+
content.fileDiffActiveChanged.connect(onAvailable);
|
|
750
|
+
setStyle(content.diffStyle);
|
|
751
|
+
setAvailable(content.fileDiffActive);
|
|
752
|
+
return () => {
|
|
753
|
+
content.diffStyleChanged.disconnect(onStyle);
|
|
754
|
+
content.fileDiffActiveChanged.disconnect(onAvailable);
|
|
755
|
+
};
|
|
756
|
+
}, [content]);
|
|
757
|
+
|
|
758
|
+
return (
|
|
759
|
+
<DiffStyleControl
|
|
760
|
+
diffStyle={style}
|
|
761
|
+
available={available}
|
|
762
|
+
onChange={next => content.setDiffStyle(next)}
|
|
763
|
+
/>
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
|
|
630
767
|
function formatTitle(change: IFileChange): string {
|
|
631
768
|
const name = change.path.split('/').pop() ?? change.path;
|
|
632
769
|
const groupLabel = change.group === 'staged' ? 'Staged' : 'Working';
|
package/style/base.css
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
--trees-bg-muted-override: var(--jp-layout-color2);
|
|
22
22
|
--trees-fg-override: var(--jp-ui-font-color1);
|
|
23
23
|
--trees-fg-muted-override: var(--jp-ui-font-color2);
|
|
24
|
+
--trees-status-ignored-override: var(--jp-ui-font-color2);
|
|
24
25
|
--trees-accent-override: var(--jp-brand-color1);
|
|
25
26
|
--trees-border-color-override: var(--jp-border-color2);
|
|
26
27
|
--trees-indent-guide-bg-override: var(--jp-border-color3);
|
package/style/git.css
CHANGED
|
@@ -79,16 +79,23 @@
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
82
|
+
* Segmented selectors that live in the diff toolbar: the Notebook/JSON
|
|
83
|
+
* view-mode toggle (for `.ipynb` diffs) and the Split/Unified diff-style
|
|
84
|
+
* toggle (for textual/code diffs). The host toolbar provides its own
|
|
85
|
+
* background and border, so these are just the segmented controls.
|
|
85
86
|
*/
|
|
86
|
-
.jp-xtralab-DiffWidget-viewModeToolbarItem
|
|
87
|
+
.jp-xtralab-DiffWidget-viewModeToolbarItem,
|
|
88
|
+
.jp-xtralab-DiffWidget-diffStyleToolbarItem {
|
|
87
89
|
display: flex;
|
|
88
90
|
align-items: center;
|
|
89
91
|
font-size: var(--jp-ui-font-size0);
|
|
90
92
|
}
|
|
91
93
|
|
|
94
|
+
/* Breathing room when both toggles appear (notebook diff in JSON view). */
|
|
95
|
+
.jp-xtralab-DiffWidget-diffStyleToolbarItem {
|
|
96
|
+
margin-left: 4px;
|
|
97
|
+
}
|
|
98
|
+
|
|
92
99
|
.jp-xtralab-DiffWidget-segmented {
|
|
93
100
|
display: inline-flex;
|
|
94
101
|
border: 1px solid var(--jp-border-color2);
|
|
@@ -120,6 +127,24 @@
|
|
|
120
127
|
background-color: var(--jp-brand-color1);
|
|
121
128
|
}
|
|
122
129
|
|
|
130
|
+
/*
|
|
131
|
+
* Icon variant of the segmented button, used by the Split/Unified toggle.
|
|
132
|
+
* The inline SVGs fill with `currentColor`, so the active/hover color rules
|
|
133
|
+
* above carry over to the glyph.
|
|
134
|
+
*/
|
|
135
|
+
.jp-xtralab-DiffWidget-segmentedButton-icon {
|
|
136
|
+
display: inline-flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
justify-content: center;
|
|
139
|
+
padding: 3px 8px;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.jp-xtralab-DiffWidget-segmentedButton-icon svg {
|
|
143
|
+
display: block;
|
|
144
|
+
width: 14px;
|
|
145
|
+
height: 14px;
|
|
146
|
+
}
|
|
147
|
+
|
|
123
148
|
/*
|
|
124
149
|
* Draggable column splitter. Sits on top of the diff (z-index above the
|
|
125
150
|
* content) but stays a thin strip so it doesn't visually intrude. The
|