xtralab 0.7.0 → 0.9.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.
Files changed (54) hide show
  1. package/README.md +18 -10
  2. package/lib/fileBrowser/commands.d.ts +7 -0
  3. package/lib/fileBrowser/commands.js +74 -1
  4. package/lib/fileBrowser/fileBrowser.js +4 -1
  5. package/lib/fileBrowser/icons.d.ts +5 -9
  6. package/lib/fileBrowser/icons.js +97 -13
  7. package/lib/fileBrowser/index.js +5 -3
  8. package/lib/fileTypeIcons/index.d.ts +6 -0
  9. package/lib/fileTypeIcons/index.js +144 -0
  10. package/lib/git/commands.d.ts +34 -33
  11. package/lib/git/commands.js +132 -10
  12. package/lib/git/diffModel.d.ts +14 -0
  13. package/lib/git/diffModel.js +75 -0
  14. package/lib/git/diffProvider.d.ts +0 -5
  15. package/lib/git/diffProvider.js +6 -284
  16. package/lib/git/diffSurface.d.ts +64 -25
  17. package/lib/git/diffSurface.js +84 -27
  18. package/lib/git/diffTheme.d.ts +9 -0
  19. package/lib/git/diffTheme.js +31 -0
  20. package/lib/git/diffWidget.d.ts +67 -85
  21. package/lib/git/diffWidget.js +242 -318
  22. package/lib/git/index.d.ts +1 -6
  23. package/lib/git/index.js +7 -23
  24. package/lib/git/notebookDiff.d.ts +6 -1
  25. package/lib/git/notebookDiff.js +4 -3
  26. package/lib/index.js +2 -0
  27. package/lib/terminals/model.d.ts +1 -1
  28. package/lib/terminals/model.js +1 -1
  29. package/lib/terminals/widget.d.ts +1 -1
  30. package/lib/terminals/widget.js +1 -1
  31. package/package.json +3 -3
  32. package/schema/plugin.json +6 -0
  33. package/src/fileBrowser/commands.ts +91 -2
  34. package/src/fileBrowser/fileBrowser.tsx +4 -1
  35. package/src/fileBrowser/icons.ts +103 -13
  36. package/src/fileBrowser/index.ts +9 -3
  37. package/src/fileTypeIcons/index.ts +174 -0
  38. package/src/git/commands.ts +177 -43
  39. package/src/git/diffModel.ts +102 -0
  40. package/src/git/diffProvider.tsx +8 -409
  41. package/src/git/diffSurface.tsx +187 -35
  42. package/src/git/diffTheme.ts +40 -0
  43. package/src/git/diffWidget.tsx +338 -393
  44. package/src/git/index.ts +14 -28
  45. package/src/git/notebookDiff.tsx +10 -3
  46. package/src/index.ts +2 -0
  47. package/src/terminals/model.ts +1 -1
  48. package/src/terminals/widget.tsx +1 -1
  49. package/style/base.css +1 -0
  50. package/style/git.css +40 -12
  51. package/style/index.css +1 -0
  52. package/style/index.js +1 -0
  53. package/style/launcher.css +46 -18
  54. package/style/sidebar.css +14 -0
@@ -18,22 +18,7 @@ import {
18
18
  NotebookDiffView,
19
19
  type INotebookDiffResult
20
20
  } from './notebookDiff';
21
-
22
- /**
23
- * Shared diff rendering surface used by both diff hosts in this extension:
24
- *
25
- * - the launcher dashboard's own `xtralab:git:open-diff` command
26
- * (via {@link DiffContentWidget} in `diffWidget.tsx`), and
27
- * - the `jupyterlab-git` diff providers registered into the upstream git
28
- * panel (via `XtralabDiffWidget` in `diffProvider.tsx`).
29
- *
30
- * The surface is deliberately content-source agnostic: callers resolve the
31
- * old/new text however they like (xtralab's REST helper, or a
32
- * `Git.Diff.IModel`'s `content()` getters) and hand the resolved strings in.
33
- * Everything visual — the `@pierre/diffs` split view, the cell-by-cell
34
- * notebook view, the draggable column resizer, and the per-hunk discard
35
- * affordance — lives here so the two hosts stay pixel-identical.
36
- */
21
+ import { resolveDiffTheme } from './diffTheme';
37
22
 
38
23
  /**
39
24
  * The CSS class added to the diff main-area widget. The selectors that
@@ -101,6 +86,35 @@ function writeStoredSplitRatio(ratio: number): void {
101
86
  }
102
87
  }
103
88
 
89
+ /**
90
+ * Layout for textual/code diffs, forwarded to `@pierre/diffs` as its
91
+ * `diffStyle` option. `split` is the side-by-side old|new view; `unified`
92
+ * is the single-column inline view (what diffs.com calls "stacked").
93
+ */
94
+ export type DiffStyle = 'split' | 'unified';
95
+
96
+ const DIFF_STYLE_STORAGE_KEY = 'xtralab:diff-style';
97
+
98
+ export function readStoredDiffStyle(): DiffStyle {
99
+ try {
100
+ const raw = window.localStorage.getItem(DIFF_STYLE_STORAGE_KEY);
101
+ if (raw === 'split' || raw === 'unified') {
102
+ return raw;
103
+ }
104
+ } catch {
105
+ // See readStoredSplitRatio — privacy-mode / sandboxed contexts can throw.
106
+ }
107
+ return 'split';
108
+ }
109
+
110
+ export function writeStoredDiffStyle(style: DiffStyle): void {
111
+ try {
112
+ window.localStorage.setItem(DIFF_STYLE_STORAGE_KEY, style);
113
+ } catch {
114
+ // Best-effort.
115
+ }
116
+ }
117
+
104
118
  /**
105
119
  * View modes available for `.ipynb` diffs. `notebook` is the cell-by-cell
106
120
  * rendered view; `json` is the raw nbformat JSON file diff, for inspecting
@@ -167,6 +181,14 @@ export function isDarkTheme(themeManager: IThemeManager | null): boolean {
167
181
  return document.body.dataset.jpThemeLight === 'false';
168
182
  }
169
183
 
184
+ /**
185
+ * Whether the active JupyterLab theme should keep Pierre diff highlighting.
186
+ */
187
+ export function isPierreTheme(themeManager: IThemeManager | null): boolean {
188
+ const theme = themeManager?.theme ?? null;
189
+ return theme !== null && theme.toLowerCase().includes('pierre');
190
+ }
191
+
170
192
  /**
171
193
  * Optional per-hunk discard wiring. When supplied and `enabled`, each hunk
172
194
  * in a plain-text/code diff gets an inline "discard" button; clicking it
@@ -182,15 +204,25 @@ export interface IHunkDiscard {
182
204
  }
183
205
 
184
206
  export interface IDiffSurfaceProps {
185
- /** Whether the host is still resolving the file contents. */
207
+ /**
208
+ * Whether the host is still resolving the file contents.
209
+ */
186
210
  loading: boolean;
187
- /** Fatal error message to show instead of a diff, or `null`. */
211
+ /**
212
+ * Fatal error message to show instead of a diff, or `null`.
213
+ */
188
214
  error: string | null;
189
- /** Whether the file is binary (no textual diff is rendered). */
215
+ /**
216
+ * Whether the file is binary (no textual diff is rendered).
217
+ */
190
218
  isBinary: boolean;
191
- /** Resolved old/reference text. Ignored while loading/binary/errored. */
219
+ /**
220
+ * Resolved old/reference text. Ignored while loading/binary/errored.
221
+ */
192
222
  oldText: string;
193
- /** Resolved new/challenger text. Ignored while loading/binary/errored. */
223
+ /**
224
+ * Resolved new/challenger text. Ignored while loading/binary/errored.
225
+ */
194
226
  newText: string;
195
227
  /**
196
228
  * New-side path. Drives notebook detection and the `@pierre/diffs` file
@@ -202,34 +234,54 @@ export interface IDiffSurfaceProps {
202
234
  * to {@link newName} when the host does not track a previous path.
203
235
  */
204
236
  oldName?: string;
205
- /** Whether to render with the dark `@pierre/diffs` theme. */
237
+ /**
238
+ * Whether to render with the dark `@pierre/diffs` theme.
239
+ */
206
240
  dark: boolean;
241
+ /**
242
+ * Whether the diff should keep Pierre's own syntax palette.
243
+ */
244
+ pierreTheme: boolean;
207
245
  /**
208
246
  * Rendermime registry used by the notebook view to render outputs and
209
247
  * markdown cells. May be `null` in stripped-down hosts; the notebook
210
248
  * diff falls back to a textual representation in that case.
211
249
  */
212
250
  rendermime: IRenderMimeRegistry | null;
213
- /** Current rendered-vs-JSON choice for notebook diffs (host-controlled). */
251
+ /**
252
+ * Current rendered-vs-JSON choice for notebook diffs (host-controlled).
253
+ */
214
254
  notebookViewMode: NotebookDiffViewMode;
255
+ /**
256
+ * Split vs unified layout for the textual/code file diff (host-controlled).
257
+ */
258
+ diffStyle: DiffStyle;
215
259
  /**
216
260
  * Called whenever the availability of a rendered notebook view changes,
217
261
  * so the host can show/hide its Notebook/JSON toolbar toggle.
218
262
  */
219
263
  onNotebookAvailabilityChange?: (available: boolean) => void;
264
+ /**
265
+ * Called whenever the textual/code file diff (the only view the split vs
266
+ * unified choice affects) becomes the active view, so the host can
267
+ * show/hide its Split/Unified toolbar toggle. False for image, binary,
268
+ * rendered-notebook, loading and error states.
269
+ */
270
+ onFileDiffActiveChange?: (active: boolean) => void;
220
271
  /**
221
272
  * Called after each (re)computation of the diff with the number of hunks
222
273
  * (or `null` when no textual diff exists). Hosts use this to implement
223
274
  * post-discard behaviors such as auto-closing an emptied diff.
224
275
  */
225
276
  onMetadataChange?: (info: { hunkCount: number | null }) => void;
226
- /** Optional per-hunk discard wiring; omit for a read-only diff. */
277
+ /**
278
+ * Optional per-hunk discard wiring; omit for a read-only diff.
279
+ */
227
280
  hunkDiscard?: IHunkDiscard;
228
281
  }
229
282
 
230
283
  /**
231
- * The shared, content-source-agnostic diff renderer. See the module
232
- * docstring for the division of labor between this surface and its hosts.
284
+ * Shared renderer for text, notebook and image diffs.
233
285
  */
234
286
  export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
235
287
  const {
@@ -241,9 +293,12 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
241
293
  newName,
242
294
  oldName,
243
295
  dark,
296
+ pierreTheme,
244
297
  rendermime,
245
298
  notebookViewMode,
299
+ diffStyle,
246
300
  onNotebookAvailabilityChange,
301
+ onFileDiffActiveChange,
247
302
  onMetadataChange,
248
303
  hunkDiscard
249
304
  } = props;
@@ -295,6 +350,21 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
295
350
  });
296
351
  }, [onMetadataChange, metadata]);
297
352
 
353
+ // Decide which view to render. The notebook view requires a successful
354
+ // nbformat parse; if that failed, we only show the raw JSON file diff.
355
+ // Computed before the early returns below so the file-diff-active effect
356
+ // can depend on it without violating the rules of hooks.
357
+ const hasNotebookView = notebookDiff !== null;
358
+ const showNotebookView = hasNotebookView && notebookViewMode === 'notebook';
359
+ const showFileDiff = !showNotebookView && metadata !== null;
360
+
361
+ // The split vs unified choice only affects the textual/code file diff, so
362
+ // tell the host whether that view is active to drive its Split/Unified
363
+ // toolbar toggle.
364
+ React.useEffect(() => {
365
+ onFileDiffActiveChange?.(showFileDiff);
366
+ }, [onFileDiffActiveChange, showFileDiff]);
367
+
298
368
  // Split ratio for the diff columns: fraction of width given to the
299
369
  // deletions (left) pane. Persisted across sessions so the user only
300
370
  // dials in their layout once.
@@ -461,12 +531,6 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
461
531
  </div>
462
532
  );
463
533
  }
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
534
  if (!showNotebookView && !showFileDiff) {
471
535
  return (
472
536
  <div className="jp-xtralab-DiffWidget-status">No content to diff.</div>
@@ -490,6 +554,7 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
490
554
  <NotebookDiffView
491
555
  diff={notebookDiff}
492
556
  dark={dark}
557
+ pierreTheme={pierreTheme}
493
558
  rendermime={rendermime}
494
559
  />
495
560
  ) : showFileDiff && metadata !== null ? (
@@ -506,12 +571,12 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
506
571
  // fine for the small files git diffs typically operate on.
507
572
  disableWorkerPool={true}
508
573
  options={{
509
- diffStyle: 'split',
574
+ diffStyle,
510
575
  // Drop the file header — the tab title already shows the
511
576
  // file name and the panel header carries the change
512
577
  // context.
513
578
  disableFileHeader: true,
514
- theme: dark ? 'pierre-dark' : 'pierre-light',
579
+ theme: resolveDiffTheme(dark, pierreTheme),
515
580
  themeType: dark ? 'dark' : 'light',
516
581
  // Inject the column-resize override into the shadow root
517
582
  // via the library's `@layer unsafe` channel. Keeping the
@@ -523,7 +588,7 @@ export function DiffSurface(props: IDiffSurfaceProps): React.ReactElement {
523
588
  />
524
589
  ) : null}
525
590
  </div>
526
- {showFileDiff ? (
591
+ {showFileDiff && diffStyle === 'split' ? (
527
592
  <div
528
593
  className="jp-xtralab-DiffWidget-resizer"
529
594
  style={{ left: `${leftPercent}%` }}
@@ -586,3 +651,90 @@ export function NotebookViewModeControl(props: {
586
651
  </div>
587
652
  );
588
653
  }
654
+
655
+ /**
656
+ * Split / unified glyphs, inlined from the `@pierre/diffs` icon sprite
657
+ * (`diffs-icon-diff-split` / `diffs-icon-diff-unified`) so the toggle looks
658
+ * like the one on the library's own docs site without depending on the
659
+ * sprite sheet being injected into the document. `currentColor` lets the
660
+ * segmented-button styling drive the fill (including the active state).
661
+ */
662
+ function DiffSplitIcon(): React.ReactElement {
663
+ return (
664
+ <svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
665
+ <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" />
666
+ <path
667
+ 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"
668
+ opacity=".3"
669
+ />
670
+ </svg>
671
+ );
672
+ }
673
+
674
+ function DiffUnifiedIcon(): React.ReactElement {
675
+ return (
676
+ <svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
677
+ <path
678
+ fillRule="evenodd"
679
+ 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"
680
+ clipRule="evenodd"
681
+ />
682
+ <path
683
+ fillRule="evenodd"
684
+ 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"
685
+ clipRule="evenodd"
686
+ opacity=".4"
687
+ />
688
+ </svg>
689
+ );
690
+ }
691
+
692
+ /**
693
+ * Segmented Split/Unified selector. Mirrors {@link NotebookViewModeControl}:
694
+ * hosts mount it into their own toolbar and drive its value/visibility from
695
+ * the same state they pass to {@link DiffSurface}. Only meaningful while the
696
+ * textual/code file diff is the active view, so `available` mirrors the
697
+ * surface's `onFileDiffActiveChange`.
698
+ */
699
+ export function DiffStyleControl(props: {
700
+ diffStyle: DiffStyle;
701
+ available: boolean;
702
+ onChange: (style: DiffStyle) => void;
703
+ }): React.ReactElement {
704
+ const { diffStyle, available, onChange } = props;
705
+ if (!available) {
706
+ return <></>;
707
+ }
708
+ return (
709
+ <div
710
+ className="jp-xtralab-DiffWidget-segmented"
711
+ role="tablist"
712
+ aria-label="Diff view style"
713
+ >
714
+ <button
715
+ type="button"
716
+ role="tab"
717
+ aria-selected={diffStyle === 'split'}
718
+ data-active={diffStyle === 'split'}
719
+ className="jp-xtralab-DiffWidget-segmentedButton jp-xtralab-DiffWidget-segmentedButton-icon"
720
+ title="Split (side-by-side) view"
721
+ aria-label="Split view"
722
+ onClick={() => onChange('split')}
723
+ >
724
+ <DiffSplitIcon />
725
+ </button>
726
+ <button
727
+ type="button"
728
+ role="tab"
729
+ aria-selected={diffStyle === 'unified'}
730
+ data-active={diffStyle === 'unified'}
731
+ className="jp-xtralab-DiffWidget-segmentedButton jp-xtralab-DiffWidget-segmentedButton-icon"
732
+ title="Unified (inline) view"
733
+ aria-label="Unified view"
734
+ onClick={() => onChange('unified')}
735
+ >
736
+ <DiffUnifiedIcon />
737
+ </button>
738
+ </div>
739
+ );
740
+ }
@@ -0,0 +1,40 @@
1
+ import {
2
+ registerCustomCSSVariableTheme,
3
+ type DiffsThemeNames
4
+ } from '@pierre/diffs';
5
+
6
+ /**
7
+ * Shiki CSS-variable theme used for non-Pierre JupyterLab themes.
8
+ */
9
+ export const JUPYTERLAB_DIFF_THEME: DiffsThemeNames = 'jupyterlab';
10
+
11
+ /**
12
+ * Register a Shiki theme whose token colors read JupyterLab's editor
13
+ * variables. The values are CSS variables, so theme changes recolor the diff.
14
+ */
15
+ registerCustomCSSVariableTheme(JUPYTERLAB_DIFF_THEME, {
16
+ foreground: 'var(--jp-content-font-color1)',
17
+ background: 'var(--jp-layout-color1)',
18
+ 'token-keyword': 'var(--jp-mirror-editor-keyword-color)',
19
+ 'token-string': 'var(--jp-mirror-editor-string-color)',
20
+ 'token-string-expression': 'var(--jp-mirror-editor-string-2-color)',
21
+ 'token-comment': 'var(--jp-mirror-editor-comment-color)',
22
+ 'token-constant': 'var(--jp-mirror-editor-number-color)',
23
+ 'token-function': 'var(--jp-mirror-editor-def-color)',
24
+ 'token-parameter': 'var(--jp-mirror-editor-variable-color)',
25
+ 'token-punctuation': 'var(--jp-mirror-editor-punctuation-color)',
26
+ 'token-link': 'var(--jp-mirror-editor-link-color)'
27
+ });
28
+
29
+ /**
30
+ * Choose Pierre's palette or the JupyterLab CSS-variable theme.
31
+ */
32
+ export function resolveDiffTheme(
33
+ dark: boolean,
34
+ pierreTheme: boolean
35
+ ): DiffsThemeNames {
36
+ if (pierreTheme) {
37
+ return dark ? 'pierre-dark' : 'pierre-light';
38
+ }
39
+ return JUPYTERLAB_DIFF_THEME;
40
+ }