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.
Files changed (39) hide show
  1. package/README.md +58 -33
  2. package/lib/fileBrowser/commands.d.ts +1 -0
  3. package/lib/fileBrowser/commands.js +46 -1
  4. package/lib/fileBrowser/fileBrowser.js +4 -8
  5. package/lib/fileBrowser/gitStatus.d.ts +16 -5
  6. package/lib/fileBrowser/gitStatus.js +28 -17
  7. package/lib/fileBrowser/widget.d.ts +15 -0
  8. package/lib/fileBrowser/widget.js +13 -0
  9. package/lib/git/commands.d.ts +5 -0
  10. package/lib/git/commands.js +4 -3
  11. package/lib/git/diffPreferences.d.ts +90 -0
  12. package/lib/git/diffPreferences.js +107 -0
  13. package/lib/git/diffProvider.js +10 -2
  14. package/lib/git/diffSurface.d.ts +34 -19
  15. package/lib/git/diffSurface.js +49 -115
  16. package/lib/git/diffTheme.d.ts +7 -0
  17. package/lib/git/diffTheme.js +23 -0
  18. package/lib/git/diffWidget.d.ts +7 -30
  19. package/lib/git/diffWidget.js +56 -100
  20. package/lib/git/imageDiff.d.ts +9 -0
  21. package/lib/git/imageDiff.js +2 -28
  22. package/lib/git/index.js +41 -2
  23. package/lib/git/notebookDiff.js +3 -2
  24. package/package.json +5 -3
  25. package/schema/git-diff-preferences.json +42 -0
  26. package/src/fileBrowser/commands.ts +64 -0
  27. package/src/fileBrowser/fileBrowser.tsx +4 -9
  28. package/src/fileBrowser/gitStatus.ts +33 -20
  29. package/src/fileBrowser/widget.tsx +22 -0
  30. package/src/git/commands.ts +13 -1
  31. package/src/git/diffPreferences.ts +192 -0
  32. package/src/git/diffProvider.tsx +11 -2
  33. package/src/git/diffSurface.tsx +113 -146
  34. package/src/git/diffTheme.ts +24 -0
  35. package/src/git/diffWidget.tsx +108 -138
  36. package/src/git/imageDiff.tsx +12 -34
  37. package/src/git/index.ts +51 -2
  38. package/src/git/notebookDiff.tsx +3 -2
  39. package/style/git.css +4 -2
@@ -1,7 +1,13 @@
1
1
  import type { GitStatus, GitStatusEntry } from '@pierre/trees';
2
2
 
3
3
  import { expandStatusFiles, status } from '../git/api';
4
- import type { FileChangeStatus } from '../git/tokens';
4
+ import type { FileChangeStatus, IFileChange } from '../git/tokens';
5
+
6
+ /**
7
+ * Repo path for `/git/*` calls; empty means the server root, letting git
8
+ * resolve the enclosing repo (same convention as the git panel).
9
+ */
10
+ export const GIT_REPO_PATH = '';
5
11
 
6
12
  /**
7
13
  * Translate a `jupyterlab_git` porcelain status into a `@pierre/trees`
@@ -25,32 +31,39 @@ function toTreeStatus(value: FileChangeStatus): GitStatus {
25
31
  }
26
32
 
27
33
  /**
28
- * Fetch the porcelain status for `repoPath` as `GitStatusEntry` values.
29
- * Any failure yields an empty array — the file browser must work without
30
- * git. A file both staged and unstaged gets one entry, the unstaged status
31
- * winning (mirrors what is on disk, matching VS Code).
34
+ * Fetch the porcelain status for `repoPath`, one entry per staged or
35
+ * unstaged change. Any failure yields an empty array — the file browser
36
+ * must work without git.
32
37
  */
33
- export async function loadGitStatusEntries(
34
- repoPath: string
35
- ): Promise<GitStatusEntry[]> {
38
+ export async function loadGitChanges(repoPath: string): Promise<IFileChange[]> {
36
39
  try {
37
40
  const result = await status(repoPath);
38
41
  if (result.code !== 0) {
39
42
  return [];
40
43
  }
41
- const changes = expandStatusFiles(result.files);
42
- const byPath = new Map<string, FileChangeStatus>();
43
- for (const change of changes) {
44
- if (change.group === 'unstaged' || !byPath.has(change.path)) {
45
- byPath.set(change.path, change.status);
46
- }
47
- }
48
- const entries: GitStatusEntry[] = [];
49
- byPath.forEach((value, path) => {
50
- entries.push({ path, status: toTreeStatus(value) });
51
- });
52
- return entries;
44
+ return expandStatusFiles(result.files);
53
45
  } catch {
54
46
  return [];
55
47
  }
56
48
  }
49
+
50
+ /**
51
+ * Collapse changes into the tree's `GitStatusEntry` values. A file both
52
+ * staged and unstaged gets one entry, the unstaged status winning (mirrors
53
+ * what is on disk, matching VS Code).
54
+ */
55
+ export function toGitStatusEntries(
56
+ changes: readonly IFileChange[]
57
+ ): GitStatusEntry[] {
58
+ const byPath = new Map<string, FileChangeStatus>();
59
+ for (const change of changes) {
60
+ if (change.group === 'unstaged' || !byPath.has(change.path)) {
61
+ byPath.set(change.path, change.status);
62
+ }
63
+ }
64
+ const entries: GitStatusEntry[] = [];
65
+ byPath.forEach((value, path) => {
66
+ entries.push({ path, status: toTreeStatus(value) });
67
+ });
68
+ return entries;
69
+ }
@@ -17,6 +17,7 @@ import {
17
17
  import { Signal, ISignal } from '@lumino/signaling';
18
18
  import { AccordionPanel, PanelLayout, Widget } from '@lumino/widgets';
19
19
 
20
+ import type { IFileChange } from '../git/tokens';
20
21
  import { FileBrowserComponent } from './fileBrowser';
21
22
  import { xtralabFileBrowserIcon } from './icons';
22
23
 
@@ -92,6 +93,12 @@ export interface IXtralabFileBrowser {
92
93
  */
93
94
  readonly selectionChanged: ISignal<IXtralabFileBrowser, readonly string[]>;
94
95
 
96
+ /**
97
+ * Git changes from the latest status poll, one per staged or unstaged
98
+ * change; paths are repo-relative.
99
+ */
100
+ readonly gitChanges: readonly IFileChange[];
101
+
95
102
  /**
96
103
  * Emits when {@link refresh} is called.
97
104
  */
@@ -251,6 +258,13 @@ export class XtralabFileBrowser
251
258
  return this._selectionChanged;
252
259
  }
253
260
 
261
+ /**
262
+ * Git changes from the latest status poll.
263
+ */
264
+ get gitChanges(): readonly IFileChange[] {
265
+ return this._gitChanges;
266
+ }
267
+
254
268
  /**
255
269
  * A signal emitted when {@link refresh} is called.
256
270
  */
@@ -308,6 +322,13 @@ export class XtralabFileBrowser
308
322
  this._selectionChanged.emit(paths);
309
323
  }
310
324
 
325
+ /**
326
+ * Update the cached git changes from the React tree's status poll.
327
+ */
328
+ updateGitChanges(changes: readonly IFileChange[]): void {
329
+ this._gitChanges = changes;
330
+ }
331
+
311
332
  /**
312
333
  * Trigger a refresh of every loaded directory in the tree.
313
334
  */
@@ -537,6 +558,7 @@ export class XtralabFileBrowser
537
558
  private _translator: ITranslator | undefined;
538
559
  private _selectedPaths: readonly string[] = [];
539
560
  private _selectionChanged = new Signal<this, readonly string[]>(this);
561
+ private _gitChanges: readonly IFileChange[] = [];
540
562
  private _refreshRequested = new Signal<this, void>(this);
541
563
  private _pathAdded = new Signal<this, string>(this);
542
564
  private _revealRequested = new Signal<this, string>(this);
@@ -10,6 +10,7 @@ import { ISignal, Signal } from '@lumino/signaling';
10
10
  import type { IAskAgent } from '../askAgent/tokens';
11
11
  import { getTreeIcon } from '../fileBrowser/icons';
12
12
  import { fileChangeToDiffModel } from './diffModel';
13
+ import type { IDiffPreferences } from './diffPreferences';
13
14
  import {
14
15
  DIFF_WIDGET_CSS_CLASS,
15
16
  XtralabDiffWidget,
@@ -156,6 +157,10 @@ interface ICreateDiffWidgetOptions {
156
157
  * Ask-agent popup for prompting an agent about diff lines; may be `null`.
157
158
  */
158
159
  askAgent: IAskAgent | null;
160
+ /**
161
+ * The display choices shared by all diff views.
162
+ */
163
+ preferences: IDiffPreferences;
159
164
  /**
160
165
  * Translation bundle for user-facing strings.
161
166
  */
@@ -180,12 +185,13 @@ function createDiffWidget(
180
185
  contentsManager,
181
186
  rendermime,
182
187
  askAgent,
188
+ preferences,
183
189
  trans
184
190
  } = options;
185
191
  const pinned = options.pinned === true;
186
192
  const content = new XtralabDiffWidget(
187
193
  fileChangeToDiffModel(repoPath, change),
188
- { contentsManager, rendermime, themeManager, askAgent, trans }
194
+ { contentsManager, rendermime, themeManager, askAgent, preferences, trans }
189
195
  );
190
196
  const widget = new DiffMainAreaWidget({ content }, change, pinned, trans);
191
197
  widget.id = pinned ? pinnedDiffWidgetId(change) : PREVIEW_DIFF_WIDGET_ID;
@@ -279,6 +285,10 @@ interface IRegisterGitCommandsOptions {
279
285
  * Ask-agent popup for prompting an agent about diff lines; may be `null`.
280
286
  */
281
287
  askAgent: IAskAgent | null;
288
+ /**
289
+ * The display choices shared by all diff views.
290
+ */
291
+ preferences: IDiffPreferences;
282
292
  /**
283
293
  * Translation bundle for user-facing strings.
284
294
  */
@@ -309,6 +319,7 @@ export function registerGitCommands(
309
319
  contentsManager,
310
320
  rendermime,
311
321
  askAgent,
322
+ preferences,
312
323
  trans,
313
324
  trackDiff,
314
325
  findDiff,
@@ -340,6 +351,7 @@ export function registerGitCommands(
340
351
  contentsManager,
341
352
  rendermime,
342
353
  askAgent,
354
+ preferences,
343
355
  trans,
344
356
  pinned: pin,
345
357
  onPinned
@@ -0,0 +1,192 @@
1
+ import * as React from 'react';
2
+
3
+ import type { ISettingRegistry } from '@jupyterlab/settingregistry';
4
+ import type { IStateDB } from '@jupyterlab/statedb';
5
+ import { Token } from '@lumino/coreutils';
6
+ import { ISignal, Signal } from '@lumino/signaling';
7
+
8
+ export type DiffStyle = 'split' | 'unified';
9
+
10
+ export type DiffStyleControlMode = 'buttons' | 'toggle';
11
+
12
+ export type NotebookDiffViewMode = 'notebook' | 'json';
13
+
14
+ export type ImageDiffViewMode = '2-up' | 'swipe' | 'onion';
15
+
16
+ export const MIN_SPLIT_RATIO = 0.1;
17
+ export const MAX_SPLIT_RATIO = 0.9;
18
+ export const DEFAULT_SPLIT_RATIO = 0.5;
19
+
20
+ const SPLIT_RATIO_STATE_KEY = 'xtralab:git-diff:split-ratio';
21
+
22
+ /**
23
+ * Display choices shared by every diff view. The toggles live in the
24
+ * plugin settings; the split width lives in the state database.
25
+ */
26
+ export interface IDiffPreferences {
27
+ /**
28
+ * The current values; a new object after each change.
29
+ */
30
+ readonly values: IDiffPreferences.IValues;
31
+ /**
32
+ * A signal emitted when any value changes.
33
+ */
34
+ readonly changed: ISignal<IDiffPreferences, void>;
35
+ /**
36
+ * Change some values and persist them.
37
+ */
38
+ update(values: Partial<IDiffPreferences.IValues>): void;
39
+ }
40
+
41
+ export namespace IDiffPreferences {
42
+ /**
43
+ * The diff display values.
44
+ */
45
+ export interface IValues {
46
+ /**
47
+ * Split or unified layout for textual file diffs.
48
+ */
49
+ diffStyle: DiffStyle;
50
+ /**
51
+ * Two toolbar buttons for the layout, or one button that toggles it.
52
+ */
53
+ diffStyleControl: DiffStyleControlMode;
54
+ /**
55
+ * Whether long lines wrap instead of scrolling horizontally.
56
+ */
57
+ lineWrap: boolean;
58
+ /**
59
+ * Rendered or JSON view for notebook diffs.
60
+ */
61
+ notebookViewMode: NotebookDiffViewMode;
62
+ /**
63
+ * Comparison mode for image diffs.
64
+ */
65
+ imageViewMode: ImageDiffViewMode;
66
+ /**
67
+ * Width of the left pane in split view, as a fraction of the diff width.
68
+ */
69
+ splitRatio: number;
70
+ }
71
+ }
72
+
73
+ export const IDiffPreferences = new Token<IDiffPreferences>(
74
+ 'xtralab:IDiffPreferences',
75
+ 'Display choices shared by the git diff views.'
76
+ );
77
+
78
+ type SettingKey = Exclude<keyof IDiffPreferences.IValues, 'splitRatio'>;
79
+
80
+ const SETTING_KEYS: SettingKey[] = [
81
+ 'diffStyle',
82
+ 'diffStyleControl',
83
+ 'lineWrap',
84
+ 'notebookViewMode',
85
+ 'imageViewMode'
86
+ ];
87
+
88
+ /**
89
+ * {@link IDiffPreferences} backed by the settings registry and the state
90
+ * database. Without them it keeps the values in memory only.
91
+ */
92
+ export class DiffPreferences implements IDiffPreferences {
93
+ get values(): IDiffPreferences.IValues {
94
+ return this._values;
95
+ }
96
+
97
+ get changed(): ISignal<this, void> {
98
+ return this._changed;
99
+ }
100
+
101
+ update(values: Partial<IDiffPreferences.IValues>): void {
102
+ if (!this._apply(values)) {
103
+ return;
104
+ }
105
+ for (const key of SETTING_KEYS) {
106
+ if (key in values) {
107
+ this._settings?.set(key, this._values[key]).catch(reason => {
108
+ console.error(`xtralab: failed to save the diff ${key}`, reason);
109
+ });
110
+ }
111
+ }
112
+ if ('splitRatio' in values) {
113
+ this._state
114
+ ?.save(SPLIT_RATIO_STATE_KEY, this._values.splitRatio)
115
+ .catch(reason => {
116
+ console.error('xtralab: failed to save the diff split', reason);
117
+ });
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Read the toggles from the plugin settings and follow their changes.
123
+ */
124
+ connectSettings(settings: ISettingRegistry.ISettings): void {
125
+ this._settings = settings;
126
+ const sync = (): void => {
127
+ this._apply(settings.composite as Partial<IDiffPreferences.IValues>);
128
+ };
129
+ sync();
130
+ settings.changed.connect(sync);
131
+ }
132
+
133
+ /**
134
+ * Restore the split width from the state database and save it there.
135
+ */
136
+ async connectState(state: IStateDB): Promise<void> {
137
+ this._state = state;
138
+ const ratio = await state.fetch(SPLIT_RATIO_STATE_KEY);
139
+ if (
140
+ typeof ratio === 'number' &&
141
+ ratio >= MIN_SPLIT_RATIO &&
142
+ ratio <= MAX_SPLIT_RATIO
143
+ ) {
144
+ this._apply({ splitRatio: ratio });
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Merge values without persisting them; return whether one changed.
150
+ */
151
+ private _apply(values: Partial<IDiffPreferences.IValues>): boolean {
152
+ const next = { ...this._values, ...values };
153
+ const keys = Object.keys(next) as (keyof IDiffPreferences.IValues)[];
154
+ if (keys.every(key => next[key] === this._values[key])) {
155
+ return false;
156
+ }
157
+ this._values = next;
158
+ this._changed.emit();
159
+ return true;
160
+ }
161
+
162
+ private _values: IDiffPreferences.IValues = {
163
+ diffStyle: 'split',
164
+ diffStyleControl: 'buttons',
165
+ lineWrap: false,
166
+ notebookViewMode: 'notebook',
167
+ imageViewMode: '2-up',
168
+ splitRatio: DEFAULT_SPLIT_RATIO
169
+ };
170
+ private _changed = new Signal<this, void>(this);
171
+ private _settings: ISettingRegistry.ISettings | null = null;
172
+ private _state: IStateDB | null = null;
173
+ }
174
+
175
+ /**
176
+ * Re-render on every preference change and return the current values.
177
+ */
178
+ export function useDiffPreferences(
179
+ preferences: IDiffPreferences
180
+ ): IDiffPreferences.IValues {
181
+ const subscribe = React.useCallback(
182
+ (onChange: () => void) => {
183
+ const slot = (): void => onChange();
184
+ preferences.changed.connect(slot);
185
+ return () => {
186
+ preferences.changed.disconnect(slot);
187
+ };
188
+ },
189
+ [preferences]
190
+ );
191
+ return React.useSyncExternalStore(subscribe, () => preferences.values);
192
+ }
@@ -9,6 +9,7 @@ import { Git, IGitExtension } from '@jupyterlab/git';
9
9
 
10
10
  import { IAskAgent } from '../askAgent/tokens';
11
11
 
12
+ import { DiffPreferences, IDiffPreferences } from './diffPreferences';
12
13
  import {
13
14
  IXtralabDiffContext,
14
15
  XtralabDiffWidget,
@@ -37,14 +38,21 @@ const diffProviderPlugin: JupyterFrontEndPlugin<void> = {
37
38
  "Replaces jupyterlab-git's notebook, text and image diff plugins with xtralab's renderer.",
38
39
  autoStart: true,
39
40
  requires: [IGitExtension],
40
- optional: [IRenderMimeRegistry, IThemeManager, ITranslator, IAskAgent],
41
+ optional: [
42
+ IRenderMimeRegistry,
43
+ IThemeManager,
44
+ ITranslator,
45
+ IAskAgent,
46
+ IDiffPreferences
47
+ ],
41
48
  activate: (
42
49
  app: JupyterFrontEnd,
43
50
  gitExtension: IGitExtension,
44
51
  rendermime: IRenderMimeRegistry | null,
45
52
  themeManager: IThemeManager | null,
46
53
  translator: ITranslator | null,
47
- askAgent: IAskAgent | null
54
+ askAgent: IAskAgent | null,
55
+ preferences: IDiffPreferences | null
48
56
  ): void => {
49
57
  const trans = (translator ?? nullTranslator).load('jupyterlab');
50
58
  const factory = makeXtralabDiffFactory({
@@ -52,6 +60,7 @@ const diffProviderPlugin: JupyterFrontEndPlugin<void> = {
52
60
  rendermime,
53
61
  themeManager,
54
62
  askAgent,
63
+ preferences: preferences ?? new DiffPreferences(),
55
64
  trans
56
65
  });
57
66