xtralab 0.15.1 → 0.15.2
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 +58 -33
- package/lib/fileBrowser/commands.d.ts +1 -0
- package/lib/fileBrowser/commands.js +46 -1
- package/lib/fileBrowser/fileBrowser.js +4 -8
- package/lib/fileBrowser/gitStatus.d.ts +16 -5
- package/lib/fileBrowser/gitStatus.js +28 -17
- package/lib/fileBrowser/widget.d.ts +15 -0
- package/lib/fileBrowser/widget.js +13 -0
- package/lib/git/commands.d.ts +5 -0
- package/lib/git/commands.js +4 -3
- package/lib/git/diffPreferences.d.ts +90 -0
- package/lib/git/diffPreferences.js +107 -0
- package/lib/git/diffProvider.js +10 -2
- package/lib/git/diffSurface.d.ts +34 -19
- package/lib/git/diffSurface.js +49 -115
- package/lib/git/diffTheme.d.ts +7 -0
- package/lib/git/diffTheme.js +23 -0
- package/lib/git/diffWidget.d.ts +7 -30
- package/lib/git/diffWidget.js +56 -100
- package/lib/git/imageDiff.d.ts +9 -0
- package/lib/git/imageDiff.js +2 -28
- package/lib/git/index.js +41 -2
- package/lib/git/notebookDiff.js +3 -2
- package/package.json +5 -3
- package/schema/git-diff-preferences.json +42 -0
- package/src/fileBrowser/commands.ts +64 -0
- package/src/fileBrowser/fileBrowser.tsx +4 -9
- package/src/fileBrowser/gitStatus.ts +33 -20
- package/src/fileBrowser/widget.tsx +22 -0
- package/src/git/commands.ts +13 -1
- package/src/git/diffPreferences.ts +192 -0
- package/src/git/diffProvider.tsx +11 -2
- package/src/git/diffSurface.tsx +113 -146
- package/src/git/diffTheme.ts +24 -0
- package/src/git/diffWidget.tsx +108 -138
- package/src/git/imageDiff.tsx +12 -34
- package/src/git/index.ts +51 -2
- package/src/git/notebookDiff.tsx +3 -2
- package/style/git.css +4 -2
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Token } from '@lumino/coreutils';
|
|
3
|
+
import { Signal } from '@lumino/signaling';
|
|
4
|
+
export const MIN_SPLIT_RATIO = 0.1;
|
|
5
|
+
export const MAX_SPLIT_RATIO = 0.9;
|
|
6
|
+
export const DEFAULT_SPLIT_RATIO = 0.5;
|
|
7
|
+
const SPLIT_RATIO_STATE_KEY = 'xtralab:git-diff:split-ratio';
|
|
8
|
+
export const IDiffPreferences = new Token('xtralab:IDiffPreferences', 'Display choices shared by the git diff views.');
|
|
9
|
+
const SETTING_KEYS = [
|
|
10
|
+
'diffStyle',
|
|
11
|
+
'diffStyleControl',
|
|
12
|
+
'lineWrap',
|
|
13
|
+
'notebookViewMode',
|
|
14
|
+
'imageViewMode'
|
|
15
|
+
];
|
|
16
|
+
/**
|
|
17
|
+
* {@link IDiffPreferences} backed by the settings registry and the state
|
|
18
|
+
* database. Without them it keeps the values in memory only.
|
|
19
|
+
*/
|
|
20
|
+
export class DiffPreferences {
|
|
21
|
+
constructor() {
|
|
22
|
+
this._values = {
|
|
23
|
+
diffStyle: 'split',
|
|
24
|
+
diffStyleControl: 'buttons',
|
|
25
|
+
lineWrap: false,
|
|
26
|
+
notebookViewMode: 'notebook',
|
|
27
|
+
imageViewMode: '2-up',
|
|
28
|
+
splitRatio: DEFAULT_SPLIT_RATIO
|
|
29
|
+
};
|
|
30
|
+
this._changed = new Signal(this);
|
|
31
|
+
this._settings = null;
|
|
32
|
+
this._state = null;
|
|
33
|
+
}
|
|
34
|
+
get values() {
|
|
35
|
+
return this._values;
|
|
36
|
+
}
|
|
37
|
+
get changed() {
|
|
38
|
+
return this._changed;
|
|
39
|
+
}
|
|
40
|
+
update(values) {
|
|
41
|
+
var _a, _b;
|
|
42
|
+
if (!this._apply(values)) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
for (const key of SETTING_KEYS) {
|
|
46
|
+
if (key in values) {
|
|
47
|
+
(_a = this._settings) === null || _a === void 0 ? void 0 : _a.set(key, this._values[key]).catch(reason => {
|
|
48
|
+
console.error(`xtralab: failed to save the diff ${key}`, reason);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if ('splitRatio' in values) {
|
|
53
|
+
(_b = this._state) === null || _b === void 0 ? void 0 : _b.save(SPLIT_RATIO_STATE_KEY, this._values.splitRatio).catch(reason => {
|
|
54
|
+
console.error('xtralab: failed to save the diff split', reason);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Read the toggles from the plugin settings and follow their changes.
|
|
60
|
+
*/
|
|
61
|
+
connectSettings(settings) {
|
|
62
|
+
this._settings = settings;
|
|
63
|
+
const sync = () => {
|
|
64
|
+
this._apply(settings.composite);
|
|
65
|
+
};
|
|
66
|
+
sync();
|
|
67
|
+
settings.changed.connect(sync);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Restore the split width from the state database and save it there.
|
|
71
|
+
*/
|
|
72
|
+
async connectState(state) {
|
|
73
|
+
this._state = state;
|
|
74
|
+
const ratio = await state.fetch(SPLIT_RATIO_STATE_KEY);
|
|
75
|
+
if (typeof ratio === 'number' &&
|
|
76
|
+
ratio >= MIN_SPLIT_RATIO &&
|
|
77
|
+
ratio <= MAX_SPLIT_RATIO) {
|
|
78
|
+
this._apply({ splitRatio: ratio });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Merge values without persisting them; return whether one changed.
|
|
83
|
+
*/
|
|
84
|
+
_apply(values) {
|
|
85
|
+
const next = { ...this._values, ...values };
|
|
86
|
+
const keys = Object.keys(next);
|
|
87
|
+
if (keys.every(key => next[key] === this._values[key])) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
this._values = next;
|
|
91
|
+
this._changed.emit();
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Re-render on every preference change and return the current values.
|
|
97
|
+
*/
|
|
98
|
+
export function useDiffPreferences(preferences) {
|
|
99
|
+
const subscribe = React.useCallback((onChange) => {
|
|
100
|
+
const slot = () => onChange();
|
|
101
|
+
preferences.changed.connect(slot);
|
|
102
|
+
return () => {
|
|
103
|
+
preferences.changed.disconnect(slot);
|
|
104
|
+
};
|
|
105
|
+
}, [preferences]);
|
|
106
|
+
return React.useSyncExternalStore(subscribe, () => preferences.values);
|
|
107
|
+
}
|
package/lib/git/diffProvider.js
CHANGED
|
@@ -3,6 +3,7 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
|
3
3
|
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
4
4
|
import { IGitExtension } from '@jupyterlab/git';
|
|
5
5
|
import { IAskAgent } from '../askAgent/tokens';
|
|
6
|
+
import { DiffPreferences, IDiffPreferences } from './diffPreferences';
|
|
6
7
|
import { XtralabDiffWidget, addDiffToolbarItems } from './diffWidget';
|
|
7
8
|
import { IMAGE_DIFF_EXTENSIONS } from './imageDiff';
|
|
8
9
|
/**
|
|
@@ -22,14 +23,21 @@ const diffProviderPlugin = {
|
|
|
22
23
|
description: "Replaces jupyterlab-git's notebook, text and image diff plugins with xtralab's renderer.",
|
|
23
24
|
autoStart: true,
|
|
24
25
|
requires: [IGitExtension],
|
|
25
|
-
optional: [
|
|
26
|
-
|
|
26
|
+
optional: [
|
|
27
|
+
IRenderMimeRegistry,
|
|
28
|
+
IThemeManager,
|
|
29
|
+
ITranslator,
|
|
30
|
+
IAskAgent,
|
|
31
|
+
IDiffPreferences
|
|
32
|
+
],
|
|
33
|
+
activate: (app, gitExtension, rendermime, themeManager, translator, askAgent, preferences) => {
|
|
27
34
|
const trans = (translator !== null && translator !== void 0 ? translator : nullTranslator).load('jupyterlab');
|
|
28
35
|
const factory = makeXtralabDiffFactory({
|
|
29
36
|
contentsManager: app.serviceManager.contents,
|
|
30
37
|
rendermime,
|
|
31
38
|
themeManager,
|
|
32
39
|
askAgent,
|
|
40
|
+
preferences: preferences !== null && preferences !== void 0 ? preferences : new DiffPreferences(),
|
|
33
41
|
trans
|
|
34
42
|
});
|
|
35
43
|
// Use one shared factory; the surface detects notebooks/images by filename.
|
package/lib/git/diffSurface.d.ts
CHANGED
|
@@ -3,25 +3,8 @@ import { IThemeManager } from '@jupyterlab/apputils';
|
|
|
3
3
|
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
4
4
|
import type { TranslationBundle } from '@jupyterlab/translation';
|
|
5
5
|
import { type SelectedLineRange } from '@pierre/diffs';
|
|
6
|
+
import { type DiffStyle, type DiffStyleControlMode, type ImageDiffViewMode, type NotebookDiffViewMode } from './diffPreferences';
|
|
6
7
|
export declare const DIFF_WIDGET_CSS_CLASS = "jp-xtralab-DiffWidget";
|
|
7
|
-
export type DiffStyle = 'split' | 'unified';
|
|
8
|
-
/**
|
|
9
|
-
* Read the persisted diff style, defaulting to `'split'`.
|
|
10
|
-
*/
|
|
11
|
-
export declare function readStoredDiffStyle(): DiffStyle;
|
|
12
|
-
/**
|
|
13
|
-
* Persist the diff style to local storage (best-effort).
|
|
14
|
-
*/
|
|
15
|
-
export declare function writeStoredDiffStyle(style: DiffStyle): void;
|
|
16
|
-
export type NotebookDiffViewMode = 'notebook' | 'json';
|
|
17
|
-
/**
|
|
18
|
-
* Read the persisted notebook view mode, defaulting to `'notebook'`.
|
|
19
|
-
*/
|
|
20
|
-
export declare function readStoredNotebookViewMode(): NotebookDiffViewMode;
|
|
21
|
-
/**
|
|
22
|
-
* Persist the notebook view mode to local storage (best-effort).
|
|
23
|
-
*/
|
|
24
|
-
export declare function writeStoredNotebookViewMode(mode: NotebookDiffViewMode): void;
|
|
25
8
|
/**
|
|
26
9
|
* Dark-theme check via `IThemeManager`, falling back to the
|
|
27
10
|
* `data-jp-theme-light` body attribute for hosts without the token.
|
|
@@ -101,6 +84,26 @@ interface IDiffSurfaceProps {
|
|
|
101
84
|
* Split vs unified layout for the textual file diff (host-controlled).
|
|
102
85
|
*/
|
|
103
86
|
diffStyle: DiffStyle;
|
|
87
|
+
/**
|
|
88
|
+
* Whether long lines wrap instead of scrolling horizontally (host-controlled).
|
|
89
|
+
*/
|
|
90
|
+
lineWrap: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Left pane width in split view, as a fraction of the diff width (host-controlled).
|
|
93
|
+
*/
|
|
94
|
+
splitRatio: number;
|
|
95
|
+
/**
|
|
96
|
+
* Called when the user finishes resizing the split panes.
|
|
97
|
+
*/
|
|
98
|
+
onSplitRatioChange: (ratio: number) => void;
|
|
99
|
+
/**
|
|
100
|
+
* 2-up, swipe or onion view for image diffs (host-controlled).
|
|
101
|
+
*/
|
|
102
|
+
imageViewMode: ImageDiffViewMode;
|
|
103
|
+
/**
|
|
104
|
+
* Called when the user picks another image view mode.
|
|
105
|
+
*/
|
|
106
|
+
onImageViewModeChange: (mode: ImageDiffViewMode) => void;
|
|
104
107
|
/**
|
|
105
108
|
* Fires when rendered-notebook availability changes so the host can toggle its Notebook/JSON control.
|
|
106
109
|
*/
|
|
@@ -145,13 +148,25 @@ export declare function NotebookViewModeControl(props: {
|
|
|
145
148
|
trans: TranslationBundle;
|
|
146
149
|
}): React.ReactElement;
|
|
147
150
|
/**
|
|
148
|
-
*
|
|
151
|
+
* Split/Unified selector: two segmented buttons, or with `control: 'toggle'`
|
|
152
|
+
* one button that shows the layout it switches to.
|
|
149
153
|
* `available` mirrors the surface's `onFileDiffActiveChange`.
|
|
150
154
|
*/
|
|
151
155
|
export declare function DiffStyleControl(props: {
|
|
152
156
|
diffStyle: DiffStyle;
|
|
157
|
+
control: DiffStyleControlMode;
|
|
153
158
|
available: boolean;
|
|
154
159
|
onChange: (style: DiffStyle) => void;
|
|
155
160
|
trans: TranslationBundle;
|
|
156
161
|
}): React.ReactElement;
|
|
162
|
+
/**
|
|
163
|
+
* Line-wrap toggle styled as a one-button segmented control.
|
|
164
|
+
* `available` mirrors the surface's `onFileDiffActiveChange`.
|
|
165
|
+
*/
|
|
166
|
+
export declare function LineWrapControl(props: {
|
|
167
|
+
lineWrap: boolean;
|
|
168
|
+
available: boolean;
|
|
169
|
+
onChange: (wrap: boolean) => void;
|
|
170
|
+
trans: TranslationBundle;
|
|
171
|
+
}): React.ReactElement;
|
|
157
172
|
export {};
|
package/lib/git/diffSurface.js
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { undoIcon } from '@jupyterlab/ui-components';
|
|
3
3
|
import { FileDiff } from '@pierre/diffs/react';
|
|
4
|
+
import { IconDiffSplit, IconDiffUnified, IconWordWrap } from '@pierre/icons';
|
|
4
5
|
import { diffAcceptRejectHunk, parseDiffFromFile } from '@pierre/diffs';
|
|
5
6
|
import { ImageDiffView, imageDataType } from './imageDiff';
|
|
6
7
|
import { buildNotebookDiff, NotebookDiffView } from './notebookDiff';
|
|
7
|
-
import {
|
|
8
|
+
import { DEFAULT_SPLIT_RATIO, MAX_SPLIT_RATIO, MIN_SPLIT_RATIO } from './diffPreferences';
|
|
9
|
+
import { DIFF_SCROLLBAR_CSS, resolveDiffTheme } from './diffTheme';
|
|
8
10
|
export const DIFF_WIDGET_CSS_CLASS = 'jp-xtralab-DiffWidget';
|
|
9
|
-
const SPLIT_RATIO_STORAGE_KEY = 'xtralab:diff-split-ratio';
|
|
10
|
-
const MIN_SPLIT_RATIO = 0.1;
|
|
11
|
-
const MAX_SPLIT_RATIO = 0.9;
|
|
12
|
-
const DEFAULT_SPLIT_RATIO = 0.5;
|
|
13
11
|
/**
|
|
14
12
|
* Injected via the `unsafeCSS` option into the shadow root's `@layer
|
|
15
13
|
* unsafe`, beating the library's base rule that hardcodes `1fr 1fr`. Reads
|
|
@@ -19,86 +17,7 @@ const DEFAULT_SPLIT_RATIO = 0.5;
|
|
|
19
17
|
const SPLIT_RESIZE_CSS = `pre[data-diff-type="split"][data-overflow="scroll"] {
|
|
20
18
|
grid-template-columns: var(--xtralab-split-cols, 1fr 1fr);
|
|
21
19
|
}`;
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
const raw = window.localStorage.getItem(SPLIT_RATIO_STORAGE_KEY);
|
|
25
|
-
if (raw === null) {
|
|
26
|
-
return DEFAULT_SPLIT_RATIO;
|
|
27
|
-
}
|
|
28
|
-
const parsed = Number.parseFloat(raw);
|
|
29
|
-
if (Number.isFinite(parsed) &&
|
|
30
|
-
parsed >= MIN_SPLIT_RATIO &&
|
|
31
|
-
parsed <= MAX_SPLIT_RATIO) {
|
|
32
|
-
return parsed;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
catch (_a) {
|
|
36
|
-
// localStorage can throw in privacy mode or sandboxed contexts.
|
|
37
|
-
}
|
|
38
|
-
return DEFAULT_SPLIT_RATIO;
|
|
39
|
-
}
|
|
40
|
-
function writeStoredSplitRatio(ratio) {
|
|
41
|
-
try {
|
|
42
|
-
window.localStorage.setItem(SPLIT_RATIO_STORAGE_KEY, ratio.toString());
|
|
43
|
-
}
|
|
44
|
-
catch (_a) {
|
|
45
|
-
// See readStoredSplitRatio — best-effort persistence.
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
const DIFF_STYLE_STORAGE_KEY = 'xtralab:diff-style';
|
|
49
|
-
/**
|
|
50
|
-
* Read the persisted diff style, defaulting to `'split'`.
|
|
51
|
-
*/
|
|
52
|
-
export function readStoredDiffStyle() {
|
|
53
|
-
try {
|
|
54
|
-
const raw = window.localStorage.getItem(DIFF_STYLE_STORAGE_KEY);
|
|
55
|
-
if (raw === 'split' || raw === 'unified') {
|
|
56
|
-
return raw;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
catch (_a) {
|
|
60
|
-
// See readStoredSplitRatio.
|
|
61
|
-
}
|
|
62
|
-
return 'split';
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Persist the diff style to local storage (best-effort).
|
|
66
|
-
*/
|
|
67
|
-
export function writeStoredDiffStyle(style) {
|
|
68
|
-
try {
|
|
69
|
-
window.localStorage.setItem(DIFF_STYLE_STORAGE_KEY, style);
|
|
70
|
-
}
|
|
71
|
-
catch (_a) {
|
|
72
|
-
// Best-effort.
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
const NOTEBOOK_DIFF_VIEW_MODE_STORAGE_KEY = 'xtralab:notebook-diff-view-mode';
|
|
76
|
-
/**
|
|
77
|
-
* Read the persisted notebook view mode, defaulting to `'notebook'`.
|
|
78
|
-
*/
|
|
79
|
-
export function readStoredNotebookViewMode() {
|
|
80
|
-
try {
|
|
81
|
-
const raw = window.localStorage.getItem(NOTEBOOK_DIFF_VIEW_MODE_STORAGE_KEY);
|
|
82
|
-
if (raw === 'json' || raw === 'notebook') {
|
|
83
|
-
return raw;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
catch (_a) {
|
|
87
|
-
// See readStoredSplitRatio.
|
|
88
|
-
}
|
|
89
|
-
return 'notebook';
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Persist the notebook view mode to local storage (best-effort).
|
|
93
|
-
*/
|
|
94
|
-
export function writeStoredNotebookViewMode(mode) {
|
|
95
|
-
try {
|
|
96
|
-
window.localStorage.setItem(NOTEBOOK_DIFF_VIEW_MODE_STORAGE_KEY, mode);
|
|
97
|
-
}
|
|
98
|
-
catch (_a) {
|
|
99
|
-
// Best-effort.
|
|
100
|
-
}
|
|
101
|
-
}
|
|
20
|
+
const DIFF_SURFACE_CSS = `${SPLIT_RESIZE_CSS}\n${DIFF_SCROLLBAR_CSS}`;
|
|
102
21
|
/**
|
|
103
22
|
* Extension-only check; a false positive is harmless because
|
|
104
23
|
* {@link buildNotebookDiff} validates the JSON and the file diff is the fallback.
|
|
@@ -128,7 +47,7 @@ export function isPierreTheme(themeManager) {
|
|
|
128
47
|
* Shared renderer for text, notebook and image diffs.
|
|
129
48
|
*/
|
|
130
49
|
export function DiffSurface(props) {
|
|
131
|
-
const { loading, error, isBinary, oldText, newText, newName, oldName, dark, pierreTheme, rendermime, notebookViewMode, diffStyle, onNotebookAvailabilityChange, onFileDiffActiveChange, onMetadataChange, hunkDiscard, onLineAsk, trans } = props;
|
|
50
|
+
const { loading, error, isBinary, oldText, newText, newName, oldName, dark, pierreTheme, rendermime, notebookViewMode, diffStyle, lineWrap, splitRatio, onSplitRatioChange, imageViewMode, onImageViewModeChange, onNotebookAvailabilityChange, onFileDiffActiveChange, onMetadataChange, hunkDiscard, onLineAsk, trans } = props;
|
|
132
51
|
const hasContent = !loading && error === null && !isBinary;
|
|
133
52
|
// For raster images the host passes both sides as base64 in oldText/newText.
|
|
134
53
|
const imageType = React.useMemo(() => imageDataType(newName), [newName]);
|
|
@@ -167,7 +86,11 @@ export function DiffSurface(props) {
|
|
|
167
86
|
React.useEffect(() => {
|
|
168
87
|
onFileDiffActiveChange === null || onFileDiffActiveChange === void 0 ? void 0 : onFileDiffActiveChange(showFileDiff);
|
|
169
88
|
}, [onFileDiffActiveChange, showFileDiff]);
|
|
170
|
-
|
|
89
|
+
// Local while dragging; the host hears the ratio when a drag ends.
|
|
90
|
+
const [leftRatio, setLeftRatio] = React.useState(splitRatio);
|
|
91
|
+
React.useEffect(() => {
|
|
92
|
+
setLeftRatio(splitRatio);
|
|
93
|
+
}, [splitRatio]);
|
|
171
94
|
const wrapperRef = React.useRef(null);
|
|
172
95
|
// Drag listeners read the live ratio from a ref to avoid stale captures.
|
|
173
96
|
const leftRatioRef = React.useRef(leftRatio);
|
|
@@ -248,16 +171,16 @@ export function DiffSurface(props) {
|
|
|
248
171
|
handle.removeEventListener('pointermove', onPointerMove);
|
|
249
172
|
handle.removeEventListener('pointerup', onPointerEnd);
|
|
250
173
|
handle.removeEventListener('pointercancel', onPointerEnd);
|
|
251
|
-
|
|
174
|
+
onSplitRatioChange(leftRatioRef.current);
|
|
252
175
|
};
|
|
253
176
|
handle.addEventListener('pointermove', onPointerMove);
|
|
254
177
|
handle.addEventListener('pointerup', onPointerEnd);
|
|
255
178
|
handle.addEventListener('pointercancel', onPointerEnd);
|
|
256
|
-
}, []);
|
|
179
|
+
}, [onSplitRatioChange]);
|
|
257
180
|
const handleResizerDoubleClick = React.useCallback(() => {
|
|
258
181
|
setLeftRatio(DEFAULT_SPLIT_RATIO);
|
|
259
|
-
|
|
260
|
-
}, []);
|
|
182
|
+
onSplitRatioChange(DEFAULT_SPLIT_RATIO);
|
|
183
|
+
}, [onSplitRatioChange]);
|
|
261
184
|
const handleResizerKeyDown = React.useCallback((event) => {
|
|
262
185
|
const step = event.shiftKey ? 0.1 : 0.01;
|
|
263
186
|
let next = null;
|
|
@@ -281,8 +204,8 @@ export function DiffSurface(props) {
|
|
|
281
204
|
event.preventDefault();
|
|
282
205
|
const clamped = Math.max(MIN_SPLIT_RATIO, Math.min(MAX_SPLIT_RATIO, next));
|
|
283
206
|
setLeftRatio(clamped);
|
|
284
|
-
|
|
285
|
-
}, []);
|
|
207
|
+
onSplitRatioChange(clamped);
|
|
208
|
+
}, [onSplitRatioChange]);
|
|
286
209
|
if (loading) {
|
|
287
210
|
return (React.createElement("div", { className: "jp-xtralab-DiffWidget-status" }, trans.__('Loading diff…')));
|
|
288
211
|
}
|
|
@@ -294,12 +217,15 @@ export function DiffSurface(props) {
|
|
|
294
217
|
}
|
|
295
218
|
if (imageType !== null) {
|
|
296
219
|
return (React.createElement("div", { className: "jp-xtralab-DiffWidget-content" },
|
|
297
|
-
React.createElement(ImageDiffView, { reference: oldText, challenger: newText, fileType: imageType, trans: trans })));
|
|
220
|
+
React.createElement(ImageDiffView, { reference: oldText, challenger: newText, fileType: imageType, mode: imageViewMode, onModeChange: onImageViewModeChange, trans: trans })));
|
|
298
221
|
}
|
|
299
222
|
if (!showNotebookView && !showFileDiff) {
|
|
300
223
|
return (React.createElement("div", { className: "jp-xtralab-DiffWidget-status" }, trans.__('No content to diff.')));
|
|
301
224
|
}
|
|
302
225
|
const leftPercent = leftRatio * 100;
|
|
226
|
+
// The wrapped split grid sizes its gutter tracks to their content, so a
|
|
227
|
+
// percentage split cannot line up with the handle.
|
|
228
|
+
const showResizer = showFileDiff && diffStyle === 'split' && !lineWrap;
|
|
303
229
|
const hostStyle = {
|
|
304
230
|
'--xtralab-split-cols': `${leftPercent}% ${100 - leftPercent}%`
|
|
305
231
|
};
|
|
@@ -310,11 +236,12 @@ export function DiffSurface(props) {
|
|
|
310
236
|
// at a resolvable URL, so the pool crashes; run on the main thread.
|
|
311
237
|
disableWorkerPool: true, options: {
|
|
312
238
|
diffStyle,
|
|
239
|
+
overflow: lineWrap ? 'wrap' : 'scroll',
|
|
313
240
|
disableFileHeader: true,
|
|
314
241
|
theme: resolveDiffTheme(dark, pierreTheme),
|
|
315
242
|
themeType: dark ? 'dark' : 'light',
|
|
316
243
|
// Constant string lets the library skip its unsafeCSS re-render path.
|
|
317
|
-
unsafeCSS:
|
|
244
|
+
unsafeCSS: DIFF_SURFACE_CSS,
|
|
318
245
|
...(onLineAsk !== undefined
|
|
319
246
|
? {
|
|
320
247
|
enableLineSelection: true,
|
|
@@ -323,7 +250,7 @@ export function DiffSurface(props) {
|
|
|
323
250
|
}
|
|
324
251
|
: {})
|
|
325
252
|
} })) : null),
|
|
326
|
-
|
|
253
|
+
showResizer ? (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), "aria-label": trans.__('Resize the diff panes'), tabIndex: 0, title: trans.__('Drag to resize the diff panes (double-click to reset)'), onPointerDown: handleResizerPointerDown, onDoubleClick: handleResizerDoubleClick, onKeyDown: handleResizerKeyDown })) : null)));
|
|
327
254
|
}
|
|
328
255
|
/**
|
|
329
256
|
* Segmented Notebook/JSON selector. Hosts mount it into their own toolbar
|
|
@@ -339,32 +266,39 @@ export function NotebookViewModeControl(props) {
|
|
|
339
266
|
React.createElement("button", { type: "button", role: "tab", "aria-selected": mode === 'json', "data-active": mode === 'json', className: "jp-xtralab-DiffWidget-segmentedButton", onClick: () => onChange('json') }, trans.__('JSON'))));
|
|
340
267
|
}
|
|
341
268
|
/**
|
|
342
|
-
* Split
|
|
343
|
-
*
|
|
344
|
-
* `currentColor` lets the button styling drive the fill.
|
|
345
|
-
*/
|
|
346
|
-
function DiffSplitIcon() {
|
|
347
|
-
return (React.createElement("svg", { viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": "true" },
|
|
348
|
-
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" }),
|
|
349
|
-
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" })));
|
|
350
|
-
}
|
|
351
|
-
function DiffUnifiedIcon() {
|
|
352
|
-
return (React.createElement("svg", { viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": "true" },
|
|
353
|
-
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" }),
|
|
354
|
-
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" })));
|
|
355
|
-
}
|
|
356
|
-
/**
|
|
357
|
-
* Segmented Split/Unified selector; mirrors {@link NotebookViewModeControl}.
|
|
269
|
+
* Split/Unified selector: two segmented buttons, or with `control: 'toggle'`
|
|
270
|
+
* one button that shows the layout it switches to.
|
|
358
271
|
* `available` mirrors the surface's `onFileDiffActiveChange`.
|
|
359
272
|
*/
|
|
360
273
|
export function DiffStyleControl(props) {
|
|
361
|
-
const { diffStyle, available, onChange, trans } = props;
|
|
274
|
+
const { diffStyle, control, available, onChange, trans } = props;
|
|
362
275
|
if (!available) {
|
|
363
276
|
return React.createElement(React.Fragment, null);
|
|
364
277
|
}
|
|
278
|
+
if (control === 'toggle') {
|
|
279
|
+
const next = diffStyle === 'split' ? 'unified' : 'split';
|
|
280
|
+
const label = next === 'split'
|
|
281
|
+
? trans.__('Switch to split view')
|
|
282
|
+
: trans.__('Switch to unified view');
|
|
283
|
+
return (React.createElement("div", { className: "jp-xtralab-DiffWidget-segmented" },
|
|
284
|
+
React.createElement("button", { type: "button", className: "jp-xtralab-DiffWidget-segmentedButton jp-xtralab-DiffWidget-segmentedButton-icon", title: label, "aria-label": label, onClick: () => onChange(next) }, next === 'split' ? React.createElement(IconDiffSplit, null) : React.createElement(IconDiffUnified, null))));
|
|
285
|
+
}
|
|
365
286
|
return (React.createElement("div", { className: "jp-xtralab-DiffWidget-segmented", role: "tablist", "aria-label": trans.__('Diff view style') },
|
|
366
287
|
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: trans.__('Split (side-by-side) view'), "aria-label": trans.__('Split view'), onClick: () => onChange('split') },
|
|
367
|
-
React.createElement(
|
|
288
|
+
React.createElement(IconDiffSplit, null)),
|
|
368
289
|
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: trans.__('Unified (inline) view'), "aria-label": trans.__('Unified view'), onClick: () => onChange('unified') },
|
|
369
|
-
React.createElement(
|
|
290
|
+
React.createElement(IconDiffUnified, null))));
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Line-wrap toggle styled as a one-button segmented control.
|
|
294
|
+
* `available` mirrors the surface's `onFileDiffActiveChange`.
|
|
295
|
+
*/
|
|
296
|
+
export function LineWrapControl(props) {
|
|
297
|
+
const { lineWrap, available, onChange, trans } = props;
|
|
298
|
+
if (!available) {
|
|
299
|
+
return React.createElement(React.Fragment, null);
|
|
300
|
+
}
|
|
301
|
+
return (React.createElement("div", { className: "jp-xtralab-DiffWidget-segmented" },
|
|
302
|
+
React.createElement("button", { type: "button", "aria-pressed": lineWrap, "data-active": lineWrap, className: "jp-xtralab-DiffWidget-segmentedButton jp-xtralab-DiffWidget-segmentedButton-icon", title: trans.__('Wrap long lines'), "aria-label": trans.__('Wrap lines'), onClick: () => onChange(!lineWrap) },
|
|
303
|
+
React.createElement(IconWordWrap, null))));
|
|
370
304
|
}
|
package/lib/git/diffTheme.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { type DiffsThemeNames } from '@pierre/diffs';
|
|
2
|
+
/**
|
|
3
|
+
* `unsafeCSS` for a taller, always-visible horizontal scrollbar. The library
|
|
4
|
+
* default is 6px and only paints the thumb on hover, in a color close to the
|
|
5
|
+
* background, so users miss that long lines scroll. Same thumb color as the
|
|
6
|
+
* app scrollbars in chrome.css.
|
|
7
|
+
*/
|
|
8
|
+
export declare const DIFF_SCROLLBAR_CSS = ":host {\n --diffs-scrollbar-gutter-override: 10px;\n --xtralab-diff-scrollbar-thumb: color-mix(in srgb, var(--jp-ui-font-color1) 28%, transparent);\n}\n[data-code]::-webkit-scrollbar-thumb {\n background-color: var(--xtralab-diff-scrollbar-thumb);\n border-width: 2px;\n border-radius: 999px;\n}\n[data-code]::-webkit-scrollbar-thumb:hover {\n background-color: color-mix(in srgb, var(--jp-ui-font-color1) 45%, transparent);\n}\n@supports (-moz-appearance: none) {\n [data-code] {\n scrollbar-color: var(--xtralab-diff-scrollbar-thumb) transparent;\n }\n}";
|
|
2
9
|
/**
|
|
3
10
|
* Choose Pierre's palette or the JupyterLab CSS-variable theme.
|
|
4
11
|
*/
|
package/lib/git/diffTheme.js
CHANGED
|
@@ -20,6 +20,29 @@ registerCustomCSSVariableTheme(JUPYTERLAB_DIFF_THEME, {
|
|
|
20
20
|
'token-punctuation': 'var(--jp-mirror-editor-punctuation-color)',
|
|
21
21
|
'token-link': 'var(--jp-mirror-editor-link-color)'
|
|
22
22
|
});
|
|
23
|
+
/**
|
|
24
|
+
* `unsafeCSS` for a taller, always-visible horizontal scrollbar. The library
|
|
25
|
+
* default is 6px and only paints the thumb on hover, in a color close to the
|
|
26
|
+
* background, so users miss that long lines scroll. Same thumb color as the
|
|
27
|
+
* app scrollbars in chrome.css.
|
|
28
|
+
*/
|
|
29
|
+
export const DIFF_SCROLLBAR_CSS = `:host {
|
|
30
|
+
--diffs-scrollbar-gutter-override: 10px;
|
|
31
|
+
--xtralab-diff-scrollbar-thumb: color-mix(in srgb, var(--jp-ui-font-color1) 28%, transparent);
|
|
32
|
+
}
|
|
33
|
+
[data-code]::-webkit-scrollbar-thumb {
|
|
34
|
+
background-color: var(--xtralab-diff-scrollbar-thumb);
|
|
35
|
+
border-width: 2px;
|
|
36
|
+
border-radius: 999px;
|
|
37
|
+
}
|
|
38
|
+
[data-code]::-webkit-scrollbar-thumb:hover {
|
|
39
|
+
background-color: color-mix(in srgb, var(--jp-ui-font-color1) 45%, transparent);
|
|
40
|
+
}
|
|
41
|
+
@supports (-moz-appearance: none) {
|
|
42
|
+
[data-code] {
|
|
43
|
+
scrollbar-color: var(--xtralab-diff-scrollbar-thumb) transparent;
|
|
44
|
+
}
|
|
45
|
+
}`;
|
|
23
46
|
/**
|
|
24
47
|
* Choose Pierre's palette or the JupyterLab CSS-variable theme.
|
|
25
48
|
*/
|
package/lib/git/diffWidget.d.ts
CHANGED
|
@@ -8,7 +8,8 @@ import { ReactWidget } from '@jupyterlab/ui-components';
|
|
|
8
8
|
import { ISignal } from '@lumino/signaling';
|
|
9
9
|
import { Widget } from '@lumino/widgets';
|
|
10
10
|
import type { IAskAgent } from '../askAgent/tokens';
|
|
11
|
-
import {
|
|
11
|
+
import { type IDiffPreferences } from './diffPreferences';
|
|
12
|
+
import { DIFF_WIDGET_CSS_CLASS } from './diffSurface';
|
|
12
13
|
export { DIFF_WIDGET_CSS_CLASS };
|
|
13
14
|
/**
|
|
14
15
|
* `Git.Diff.IModel` plus optional xtralab-only metadata.
|
|
@@ -49,6 +50,10 @@ export interface IXtralabDiffContext {
|
|
|
49
50
|
* When available, diff line selections get an "ask an agent" gutter button.
|
|
50
51
|
*/
|
|
51
52
|
askAgent: IAskAgent | null;
|
|
53
|
+
/**
|
|
54
|
+
* The display choices shared by all diff views.
|
|
55
|
+
*/
|
|
56
|
+
preferences: IDiffPreferences;
|
|
52
57
|
/**
|
|
53
58
|
* The application translation bundle.
|
|
54
59
|
*/
|
|
@@ -79,18 +84,6 @@ export declare class XtralabDiffWidget extends ReactWidget implements Git.Diff.I
|
|
|
79
84
|
* Re-pull both sides of the model and re-render.
|
|
80
85
|
*/
|
|
81
86
|
refresh(): Promise<void>;
|
|
82
|
-
/**
|
|
83
|
-
* The current rendered-vs-JSON choice for notebook diffs.
|
|
84
|
-
*/
|
|
85
|
-
get notebookViewMode(): NotebookDiffViewMode;
|
|
86
|
-
/**
|
|
87
|
-
* Set the notebook view mode and persist it.
|
|
88
|
-
*/
|
|
89
|
-
setNotebookViewMode(mode: NotebookDiffViewMode): void;
|
|
90
|
-
/**
|
|
91
|
-
* A signal emitted when the notebook view mode changes.
|
|
92
|
-
*/
|
|
93
|
-
get notebookViewModeChanged(): ISignal<this, NotebookDiffViewMode>;
|
|
94
87
|
/**
|
|
95
88
|
* Whether a rendered notebook view is currently available.
|
|
96
89
|
*/
|
|
@@ -104,19 +97,7 @@ export declare class XtralabDiffWidget extends ReactWidget implements Git.Diff.I
|
|
|
104
97
|
*/
|
|
105
98
|
get hasNotebookViewChanged(): ISignal<this, boolean>;
|
|
106
99
|
/**
|
|
107
|
-
*
|
|
108
|
-
*/
|
|
109
|
-
get diffStyle(): DiffStyle;
|
|
110
|
-
/**
|
|
111
|
-
* Set the diff style and persist it.
|
|
112
|
-
*/
|
|
113
|
-
setDiffStyle(style: DiffStyle): void;
|
|
114
|
-
/**
|
|
115
|
-
* A signal emitted when the diff style changes.
|
|
116
|
-
*/
|
|
117
|
-
get diffStyleChanged(): ISignal<this, DiffStyle>;
|
|
118
|
-
/**
|
|
119
|
-
* Whether the Split/Unified toolbar selector currently applies.
|
|
100
|
+
* Whether the Split/Unified and line-wrap toolbar controls currently apply.
|
|
120
101
|
*/
|
|
121
102
|
get fileDiffActive(): boolean;
|
|
122
103
|
/**
|
|
@@ -159,12 +140,8 @@ export declare class XtralabDiffWidget extends ReactWidget implements Git.Diff.I
|
|
|
159
140
|
private _context;
|
|
160
141
|
private _reloadNonce;
|
|
161
142
|
private _pendingRefresh;
|
|
162
|
-
private _notebookViewMode;
|
|
163
|
-
private _notebookViewModeChanged;
|
|
164
143
|
private _hasNotebookView;
|
|
165
144
|
private _hasNotebookViewChanged;
|
|
166
|
-
private _diffStyle;
|
|
167
|
-
private _diffStyleChanged;
|
|
168
145
|
private _fileDiffActive;
|
|
169
146
|
private _fileDiffActiveChanged;
|
|
170
147
|
private _emptied;
|