xtralab 0.15.0 → 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 (237) hide show
  1. package/README.md +65 -30
  2. package/lib/about/icons.d.ts +0 -5
  3. package/lib/about/icons.js +0 -5
  4. package/lib/about/index.d.ts +4 -8
  5. package/lib/about/index.js +6 -17
  6. package/lib/agentSessions.d.ts +9 -29
  7. package/lib/agentSessions.js +2 -7
  8. package/lib/askAgent/editorSelection.d.ts +20 -26
  9. package/lib/askAgent/editorSelection.js +11 -26
  10. package/lib/askAgent/icons.d.ts +2 -3
  11. package/lib/askAgent/icons.js +2 -3
  12. package/lib/askAgent/index.d.ts +4 -26
  13. package/lib/askAgent/index.js +54 -158
  14. package/lib/askAgent/popup.d.ts +12 -16
  15. package/lib/askAgent/popup.js +18 -37
  16. package/lib/askAgent/prompt.d.ts +3 -5
  17. package/lib/askAgent/prompt.js +6 -19
  18. package/lib/askAgent/queue.d.ts +11 -21
  19. package/lib/askAgent/queue.js +9 -19
  20. package/lib/askAgent/queuePanel.d.ts +13 -18
  21. package/lib/askAgent/queuePanel.js +15 -37
  22. package/lib/askAgent/targetPicker.d.ts +2 -9
  23. package/lib/askAgent/targetPicker.js +6 -11
  24. package/lib/askAgent/tokens.d.ts +32 -46
  25. package/lib/askAgent/tokens.js +4 -6
  26. package/lib/commandBar/index.d.ts +4 -13
  27. package/lib/commandBar/index.js +21 -39
  28. package/lib/customPanel/index.js +15 -0
  29. package/lib/editorBreadcrumbs/index.d.ts +3 -6
  30. package/lib/editorBreadcrumbs/index.js +10 -6
  31. package/lib/editorBreadcrumbs/widget.d.ts +17 -4
  32. package/lib/editorBreadcrumbs/widget.js +11 -17
  33. package/lib/editorIndent/index.d.ts +4 -12
  34. package/lib/editorIndent/index.js +8 -23
  35. package/lib/fileBrowser/commands.d.ts +29 -19
  36. package/lib/fileBrowser/commands.js +83 -97
  37. package/lib/fileBrowser/contents.d.ts +3 -0
  38. package/lib/fileBrowser/dragAndDrop.d.ts +17 -4
  39. package/lib/fileBrowser/dragAndDrop.js +2 -7
  40. package/lib/fileBrowser/fileBrowser.d.ts +19 -9
  41. package/lib/fileBrowser/fileBrowser.js +70 -222
  42. package/lib/fileBrowser/gitStatus.d.ts +16 -10
  43. package/lib/fileBrowser/gitStatus.js +31 -30
  44. package/lib/fileBrowser/gitignore.d.ts +4 -17
  45. package/lib/fileBrowser/gitignore.js +4 -25
  46. package/lib/fileBrowser/icons.d.ts +7 -17
  47. package/lib/fileBrowser/icons.js +25 -80
  48. package/lib/fileBrowser/index.js +2 -4
  49. package/lib/fileBrowser/widget.d.ts +141 -48
  50. package/lib/fileBrowser/widget.js +102 -4
  51. package/lib/fileTypeIcons/index.js +0 -10
  52. package/lib/git/api.d.ts +5 -13
  53. package/lib/git/api.js +10 -36
  54. package/lib/git/askRequest.d.ts +4 -8
  55. package/lib/git/askRequest.js +6 -20
  56. package/lib/git/commands.d.ts +45 -5
  57. package/lib/git/commands.js +20 -13
  58. package/lib/git/diffModel.js +4 -0
  59. package/lib/git/diffPreferences.d.ts +90 -0
  60. package/lib/git/diffPreferences.js +107 -0
  61. package/lib/git/diffProvider.js +10 -2
  62. package/lib/git/diffSurface.d.ts +64 -63
  63. package/lib/git/diffSurface.js +72 -213
  64. package/lib/git/diffTheme.d.ts +7 -0
  65. package/lib/git/diffTheme.js +23 -0
  66. package/lib/git/diffWidget.d.ts +70 -14
  67. package/lib/git/diffWidget.js +100 -94
  68. package/lib/git/imageDiff.d.ts +14 -6
  69. package/lib/git/imageDiff.js +19 -75
  70. package/lib/git/index.d.ts +0 -3
  71. package/lib/git/index.js +44 -10
  72. package/lib/git/notebookDiff.d.ts +110 -33
  73. package/lib/git/notebookDiff.js +55 -197
  74. package/lib/git/tokens.d.ts +73 -31
  75. package/lib/git/tokens.js +2 -3
  76. package/lib/highlight/index.d.ts +4 -10
  77. package/lib/highlight/index.js +10 -41
  78. package/lib/index.d.ts +0 -9
  79. package/lib/index.js +0 -9
  80. package/lib/launcher/agents.d.ts +55 -51
  81. package/lib/launcher/agents.js +15 -57
  82. package/lib/launcher/availability.d.ts +3 -9
  83. package/lib/launcher/availability.js +3 -9
  84. package/lib/launcher/commands.d.ts +7 -32
  85. package/lib/launcher/commands.js +14 -45
  86. package/lib/launcher/dashboard.d.ts +19 -24
  87. package/lib/launcher/dashboard.js +21 -67
  88. package/lib/launcher/editorRegistry.d.ts +10 -22
  89. package/lib/launcher/editorRegistry.js +14 -18
  90. package/lib/launcher/editors.d.ts +39 -45
  91. package/lib/launcher/editors.js +8 -34
  92. package/lib/launcher/icons.d.ts +0 -12
  93. package/lib/launcher/icons.js +5 -39
  94. package/lib/launcher/index.js +12 -55
  95. package/lib/launcher/invocation.d.ts +4 -10
  96. package/lib/launcher/invocation.js +7 -22
  97. package/lib/launcher/registry.d.ts +9 -7
  98. package/lib/launcher/registry.js +9 -7
  99. package/lib/launcher/tokens.d.ts +10 -22
  100. package/lib/launcher/tokens.js +5 -8
  101. package/lib/menuBar/index.d.ts +3 -10
  102. package/lib/menuBar/index.js +11 -28
  103. package/lib/menus/index.d.ts +3 -9
  104. package/lib/menus/index.js +14 -30
  105. package/lib/omnibox/files.d.ts +2 -4
  106. package/lib/omnibox/files.js +5 -11
  107. package/lib/omnibox/index.d.ts +4 -14
  108. package/lib/omnibox/index.js +9 -27
  109. package/lib/omnibox/model.d.ts +38 -10
  110. package/lib/omnibox/model.js +15 -33
  111. package/lib/omnibox/recents.d.ts +15 -17
  112. package/lib/omnibox/recents.js +12 -17
  113. package/lib/omnibox/tokens.d.ts +5 -10
  114. package/lib/omnibox/tokens.js +2 -6
  115. package/lib/omnibox/widget.d.ts +11 -7
  116. package/lib/omnibox/widget.js +8 -16
  117. package/lib/searchReplace/index.d.ts +4 -20
  118. package/lib/searchReplace/index.js +7 -27
  119. package/lib/showOutput/index.d.ts +3 -12
  120. package/lib/showOutput/index.js +6 -22
  121. package/lib/sidebar/index.d.ts +4 -7
  122. package/lib/sidebar/index.js +5 -12
  123. package/lib/terminalNotifications/index.d.ts +4 -10
  124. package/lib/terminalNotifications/index.js +6 -20
  125. package/lib/terminals/agentTerminals.d.ts +12 -5
  126. package/lib/terminals/agentTerminals.js +25 -44
  127. package/lib/terminals/detection.d.ts +4 -8
  128. package/lib/terminals/detection.js +4 -8
  129. package/lib/terminals/index.d.ts +0 -35
  130. package/lib/terminals/index.js +12 -101
  131. package/lib/terminals/model.d.ts +64 -139
  132. package/lib/terminals/model.js +80 -220
  133. package/lib/terminals/tokens.d.ts +11 -22
  134. package/lib/terminals/tokens.js +2 -2
  135. package/lib/terminals/widget.d.ts +43 -13
  136. package/lib/terminals/widget.js +33 -18
  137. package/lib/topBar/icons.d.ts +3 -8
  138. package/lib/topBar/icons.js +3 -8
  139. package/lib/topBar/index.d.ts +4 -11
  140. package/lib/topBar/index.js +10 -38
  141. package/lib/walkthrough/index.d.ts +4 -9
  142. package/lib/walkthrough/index.js +6 -20
  143. package/lib/walkthrough/panel.d.ts +40 -9
  144. package/lib/walkthrough/panel.js +4 -10
  145. package/package.json +5 -3
  146. package/schema/git-diff-preferences.json +42 -0
  147. package/src/about/icons.ts +0 -5
  148. package/src/about/index.tsx +6 -17
  149. package/src/agentSessions.ts +9 -29
  150. package/src/askAgent/editorSelection.ts +22 -29
  151. package/src/askAgent/icons.ts +2 -3
  152. package/src/askAgent/index.ts +54 -158
  153. package/src/askAgent/popup.tsx +23 -45
  154. package/src/askAgent/prompt.ts +6 -19
  155. package/src/askAgent/queue.ts +13 -24
  156. package/src/askAgent/queuePanel.tsx +21 -46
  157. package/src/askAgent/targetPicker.tsx +5 -15
  158. package/src/askAgent/tokens.ts +32 -46
  159. package/src/commandBar/index.ts +24 -39
  160. package/src/customPanel/index.ts +15 -0
  161. package/src/editorBreadcrumbs/index.ts +10 -6
  162. package/src/editorBreadcrumbs/widget.ts +23 -17
  163. package/src/editorIndent/index.ts +17 -23
  164. package/src/fileBrowser/commands.ts +119 -96
  165. package/src/fileBrowser/contents.ts +3 -0
  166. package/src/fileBrowser/dragAndDrop.ts +17 -7
  167. package/src/fileBrowser/fileBrowser.tsx +86 -230
  168. package/src/fileBrowser/gitStatus.ts +36 -33
  169. package/src/fileBrowser/gitignore.ts +4 -25
  170. package/src/fileBrowser/icons.ts +25 -80
  171. package/src/fileBrowser/index.ts +2 -4
  172. package/src/fileBrowser/widget.tsx +171 -50
  173. package/src/fileTypeIcons/index.ts +0 -10
  174. package/src/git/api.ts +10 -36
  175. package/src/git/askRequest.ts +6 -20
  176. package/src/git/commands.ts +83 -11
  177. package/src/git/diffModel.ts +25 -0
  178. package/src/git/diffPreferences.ts +192 -0
  179. package/src/git/diffProvider.tsx +11 -2
  180. package/src/git/diffSurface.tsx +165 -286
  181. package/src/git/diffTheme.ts +24 -0
  182. package/src/git/diffWidget.tsx +194 -134
  183. package/src/git/imageDiff.tsx +56 -84
  184. package/src/git/index.ts +54 -10
  185. package/src/git/notebookDiff.tsx +214 -240
  186. package/src/git/tokens.ts +73 -31
  187. package/src/highlight/index.ts +10 -41
  188. package/src/index.ts +0 -9
  189. package/src/launcher/agents.ts +64 -95
  190. package/src/launcher/availability.ts +3 -9
  191. package/src/launcher/commands.ts +14 -51
  192. package/src/launcher/dashboard.tsx +49 -88
  193. package/src/launcher/editorRegistry.ts +21 -29
  194. package/src/launcher/editors.ts +39 -58
  195. package/src/launcher/icons.ts +5 -39
  196. package/src/launcher/index.ts +12 -55
  197. package/src/launcher/invocation.ts +7 -22
  198. package/src/launcher/registry.ts +9 -7
  199. package/src/launcher/tokens.ts +10 -22
  200. package/src/menuBar/index.ts +11 -28
  201. package/src/menus/index.ts +14 -30
  202. package/src/omnibox/files.ts +10 -14
  203. package/src/omnibox/index.ts +9 -27
  204. package/src/omnibox/model.ts +50 -38
  205. package/src/omnibox/recents.ts +17 -22
  206. package/src/omnibox/tokens.ts +5 -10
  207. package/src/omnibox/widget.tsx +14 -19
  208. package/src/searchReplace/index.ts +7 -27
  209. package/src/showOutput/index.ts +6 -22
  210. package/src/sidebar/index.ts +20 -12
  211. package/src/terminalNotifications/index.ts +42 -25
  212. package/src/terminals/agentTerminals.ts +25 -49
  213. package/src/terminals/detection.ts +4 -8
  214. package/src/terminals/index.ts +12 -101
  215. package/src/terminals/model.ts +130 -246
  216. package/src/terminals/tokens.ts +11 -22
  217. package/src/terminals/widget.tsx +48 -31
  218. package/src/topBar/icons.ts +3 -8
  219. package/src/topBar/index.ts +21 -44
  220. package/src/walkthrough/index.ts +6 -20
  221. package/src/walkthrough/panel.ts +40 -10
  222. package/style/about.css +2 -7
  223. package/style/askAgent.css +8 -40
  224. package/style/base.css +9 -45
  225. package/style/chrome.css +108 -308
  226. package/style/commandBar.css +4 -20
  227. package/style/customPanel.css +1 -4
  228. package/style/git.css +26 -186
  229. package/style/highlight.css +1 -3
  230. package/style/launcher.css +6 -70
  231. package/style/omnibox.css +2 -14
  232. package/style/showOutput.css +0 -6
  233. package/style/sidebar.css +29 -9
  234. package/style/tabs.css +5 -13
  235. package/style/terminals.css +6 -53
  236. package/style/topBar.css +2 -22
  237. package/style/walkthrough.css +0 -8
@@ -26,6 +26,30 @@ registerCustomCSSVariableTheme(JUPYTERLAB_DIFF_THEME, {
26
26
  'token-link': 'var(--jp-mirror-editor-link-color)'
27
27
  });
28
28
 
29
+ /**
30
+ * `unsafeCSS` for a taller, always-visible horizontal scrollbar. The library
31
+ * default is 6px and only paints the thumb on hover, in a color close to the
32
+ * background, so users miss that long lines scroll. Same thumb color as the
33
+ * app scrollbars in chrome.css.
34
+ */
35
+ export const DIFF_SCROLLBAR_CSS = `:host {
36
+ --diffs-scrollbar-gutter-override: 10px;
37
+ --xtralab-diff-scrollbar-thumb: color-mix(in srgb, var(--jp-ui-font-color1) 28%, transparent);
38
+ }
39
+ [data-code]::-webkit-scrollbar-thumb {
40
+ background-color: var(--xtralab-diff-scrollbar-thumb);
41
+ border-width: 2px;
42
+ border-radius: 999px;
43
+ }
44
+ [data-code]::-webkit-scrollbar-thumb:hover {
45
+ background-color: color-mix(in srgb, var(--jp-ui-font-color1) 45%, transparent);
46
+ }
47
+ @supports (-moz-appearance: none) {
48
+ [data-code] {
49
+ scrollbar-color: var(--xtralab-diff-scrollbar-thumb) transparent;
50
+ }
51
+ }`;
52
+
29
53
  /**
30
54
  * Choose Pierre's palette or the JupyterLab CSS-variable theme.
31
55
  */
@@ -15,19 +15,19 @@ import type { SelectedLineRange } from '@pierre/diffs';
15
15
  import type { IAskAgent } from '../askAgent/tokens';
16
16
 
17
17
  import { buildDiffAskRequest } from './askRequest';
18
+ import {
19
+ useDiffPreferences,
20
+ type IDiffPreferences,
21
+ type ImageDiffViewMode
22
+ } from './diffPreferences';
18
23
  import {
19
24
  DIFF_WIDGET_CSS_CLASS,
20
25
  DiffStyleControl,
21
26
  DiffSurface,
27
+ LineWrapControl,
22
28
  NotebookViewModeControl,
23
29
  isDarkTheme,
24
- isPierreTheme,
25
- readStoredDiffStyle,
26
- readStoredNotebookViewMode,
27
- writeStoredDiffStyle,
28
- writeStoredNotebookViewMode,
29
- type DiffStyle,
30
- type NotebookDiffViewMode
30
+ isPierreTheme
31
31
  } from './diffSurface';
32
32
  import { imageDataType } from './imageDiff';
33
33
 
@@ -37,8 +37,17 @@ export { DIFF_WIDGET_CSS_CLASS };
37
37
  * `Git.Diff.IModel` plus optional xtralab-only metadata.
38
38
  */
39
39
  export interface IXtralabDiffModel extends Git.Diff.IModel {
40
+ /**
41
+ * Whether the file is binary, so no text contents are fetched.
42
+ */
40
43
  isBinary?: boolean;
44
+ /**
45
+ * Whether hunks may be discarded; when absent, a working-tree heuristic decides.
46
+ */
41
47
  canDiscard?: boolean;
48
+ /**
49
+ * The old-side path when the diff represents a rename.
50
+ */
42
51
  oldFilename?: string;
43
52
  }
44
53
 
@@ -46,14 +55,31 @@ export interface IXtralabDiffModel extends Git.Diff.IModel {
46
55
  * Application-level services the diff model does not carry.
47
56
  */
48
57
  export interface IXtralabDiffContext {
58
+ /**
59
+ * The contents manager used to save hunk-discard results.
60
+ */
49
61
  contentsManager: Contents.IManager;
62
+ /**
63
+ * The rendermime registry for rendered notebook diffs, or `null` to
64
+ * force the textual fallback.
65
+ */
50
66
  rendermime: IRenderMimeRegistry | null;
67
+ /**
68
+ * The theme manager used to track theme changes, or `null` to watch
69
+ * the body attribute instead.
70
+ */
51
71
  themeManager: IThemeManager | null;
52
72
  /**
53
- * The ask-agent popup; when available, diff line selections get an
54
- * "ask an agent about these lines" gutter button.
73
+ * When available, diff line selections get an "ask an agent" gutter button.
55
74
  */
56
75
  askAgent: IAskAgent | null;
76
+ /**
77
+ * The display choices shared by all diff views.
78
+ */
79
+ preferences: IDiffPreferences;
80
+ /**
81
+ * The application translation bundle.
82
+ */
57
83
  trans: TranslationBundle;
58
84
  }
59
85
 
@@ -68,13 +94,14 @@ export class XtralabDiffWidget
68
94
  super();
69
95
  this._model = model;
70
96
  this._context = context;
71
- this._notebookViewMode = readStoredNotebookViewMode();
72
- this._diffStyle = readStoredDiffStyle();
73
97
  // Reuse jupyterlab-git's host sizing while applying xtralab styling.
74
98
  this.addClass('jp-git-diff-root');
75
99
  this.addClass(DIFF_WIDGET_CSS_CLASS);
76
100
  }
77
101
 
102
+ /**
103
+ * The git diff model being rendered.
104
+ */
78
105
  get model(): Git.Diff.IModel {
79
106
  return this._model;
80
107
  }
@@ -124,27 +151,16 @@ export class XtralabDiffWidget
124
151
  await done.promise;
125
152
  }
126
153
 
127
- get notebookViewMode(): NotebookDiffViewMode {
128
- return this._notebookViewMode;
129
- }
130
-
131
- setNotebookViewMode(mode: NotebookDiffViewMode): void {
132
- if (mode === this._notebookViewMode) {
133
- return;
134
- }
135
- this._notebookViewMode = mode;
136
- writeStoredNotebookViewMode(mode);
137
- this._notebookViewModeChanged.emit(mode);
138
- }
139
-
140
- get notebookViewModeChanged(): ISignal<this, NotebookDiffViewMode> {
141
- return this._notebookViewModeChanged;
142
- }
143
-
154
+ /**
155
+ * Whether a rendered notebook view is currently available.
156
+ */
144
157
  get hasNotebookView(): boolean {
145
158
  return this._hasNotebookView;
146
159
  }
147
160
 
161
+ /**
162
+ * Set whether a rendered notebook view is available.
163
+ */
148
164
  setHasNotebookView(value: boolean): void {
149
165
  if (value === this._hasNotebookView) {
150
166
  return;
@@ -153,34 +169,23 @@ export class XtralabDiffWidget
153
169
  this._hasNotebookViewChanged.emit(value);
154
170
  }
155
171
 
172
+ /**
173
+ * A signal emitted when rendered-notebook availability changes.
174
+ */
156
175
  get hasNotebookViewChanged(): ISignal<this, boolean> {
157
176
  return this._hasNotebookViewChanged;
158
177
  }
159
178
 
160
- get diffStyle(): DiffStyle {
161
- return this._diffStyle;
162
- }
163
-
164
- setDiffStyle(style: DiffStyle): void {
165
- if (style === this._diffStyle) {
166
- return;
167
- }
168
- this._diffStyle = style;
169
- writeStoredDiffStyle(style);
170
- this._diffStyleChanged.emit(style);
171
- }
172
-
173
- get diffStyleChanged(): ISignal<this, DiffStyle> {
174
- return this._diffStyleChanged;
175
- }
176
-
177
179
  /**
178
- * Whether the Split/Unified toolbar selector currently applies.
180
+ * Whether the Split/Unified and line-wrap toolbar controls currently apply.
179
181
  */
180
182
  get fileDiffActive(): boolean {
181
183
  return this._fileDiffActive;
182
184
  }
183
185
 
186
+ /**
187
+ * Set whether the textual file diff is the active view.
188
+ */
184
189
  setFileDiffActive(value: boolean): void {
185
190
  if (value === this._fileDiffActive) {
186
191
  return;
@@ -189,6 +194,9 @@ export class XtralabDiffWidget
189
194
  this._fileDiffActiveChanged.emit(value);
190
195
  }
191
196
 
197
+ /**
198
+ * A signal emitted when {@link fileDiffActive} changes.
199
+ */
192
200
  get fileDiffActiveChanged(): ISignal<this, boolean> {
193
201
  return this._fileDiffActiveChanged;
194
202
  }
@@ -200,32 +208,49 @@ export class XtralabDiffWidget
200
208
  return this._emptied;
201
209
  }
202
210
 
211
+ /**
212
+ * Emit the {@link emptied} signal.
213
+ */
203
214
  notifyEmptied(): void {
204
215
  this._emptied.emit();
205
216
  }
206
217
 
218
+ /**
219
+ * Resolve and clear the pending `refresh()` promise, if any.
220
+ */
207
221
  settleRefresh(): void {
208
222
  const pending = this._pendingRefresh;
209
223
  this._pendingRefresh = null;
210
224
  pending?.resolve();
211
225
  }
212
226
 
227
+ /**
228
+ * Dispose of the resources held by the widget.
229
+ */
213
230
  dispose(): void {
214
- // Release any in-flight refresh awaiter so a host that awaits refresh()
215
- // (jupyterlab-git re-shows its diff button only once it resolves) does not
216
- // hang when the widget is torn down mid-fetch.
231
+ // jupyterlab-git awaits refresh() to re-show its diff button; release any
232
+ // in-flight awaiter so it does not hang on mid-fetch teardown.
217
233
  this.settleRefresh();
218
234
  super.dispose();
219
235
  }
220
236
 
237
+ /**
238
+ * A counter incremented on each `refresh()` to trigger a content re-fetch.
239
+ */
221
240
  get reloadNonce(): number {
222
241
  return this._reloadNonce;
223
242
  }
224
243
 
244
+ /**
245
+ * The application-level services used by the rendered diff.
246
+ */
225
247
  get context(): IXtralabDiffContext {
226
248
  return this._context;
227
249
  }
228
250
 
251
+ /**
252
+ * Render the diff view for the current model.
253
+ */
229
254
  protected render(): React.ReactElement {
230
255
  return <ModelDiffView widget={this} />;
231
256
  }
@@ -234,23 +259,32 @@ export class XtralabDiffWidget
234
259
  private _context: IXtralabDiffContext;
235
260
  private _reloadNonce = 0;
236
261
  private _pendingRefresh: PromiseDelegate<void> | null = null;
237
- private _notebookViewMode: NotebookDiffViewMode;
238
- private _notebookViewModeChanged = new Signal<this, NotebookDiffViewMode>(
239
- this
240
- );
241
262
  private _hasNotebookView = false;
242
263
  private _hasNotebookViewChanged = new Signal<this, boolean>(this);
243
- private _diffStyle: DiffStyle;
244
- private _diffStyleChanged = new Signal<this, DiffStyle>(this);
245
264
  private _fileDiffActive = false;
246
265
  private _fileDiffActiveChanged = new Signal<this, boolean>(this);
247
266
  private _emptied = new Signal<this, void>(this);
248
267
  }
249
268
 
269
+ /**
270
+ * Resolved file-contents state for {@link ModelDiffView}.
271
+ */
250
272
  interface IModelDiffState {
273
+ /**
274
+ * Whether the file contents are still being fetched.
275
+ */
251
276
  loading: boolean;
277
+ /**
278
+ * The resolved reference-side text.
279
+ */
252
280
  oldText: string;
281
+ /**
282
+ * The resolved challenger-side text.
283
+ */
253
284
  newText: string;
285
+ /**
286
+ * The fetch error message, or `null` if none.
287
+ */
254
288
  error: string | null;
255
289
  }
256
290
 
@@ -259,8 +293,15 @@ function ModelDiffView(props: {
259
293
  }): React.ReactElement {
260
294
  const { widget } = props;
261
295
  const model = widget.model as IXtralabDiffModel;
262
- const { contentsManager, rendermime, themeManager, askAgent, trans } =
263
- widget.context;
296
+ const {
297
+ contentsManager,
298
+ rendermime,
299
+ themeManager,
300
+ askAgent,
301
+ preferences,
302
+ trans
303
+ } = widget.context;
304
+ const values = useDiffPreferences(preferences);
264
305
  const nonce = widget.reloadNonce;
265
306
 
266
307
  const [state, setState] = React.useState<IModelDiffState>({
@@ -270,23 +311,6 @@ function ModelDiffView(props: {
270
311
  error: null
271
312
  });
272
313
 
273
- // Mirror toolbar-driven notebook view changes into React state.
274
- const [notebookViewMode, setNotebookViewMode] =
275
- React.useState<NotebookDiffViewMode>(() => widget.notebookViewMode);
276
- React.useEffect(() => {
277
- const handler = (
278
- sender: XtralabDiffWidget,
279
- mode: NotebookDiffViewMode
280
- ): void => {
281
- setNotebookViewMode(mode);
282
- };
283
- widget.notebookViewModeChanged.connect(handler);
284
- setNotebookViewMode(widget.notebookViewMode);
285
- return () => {
286
- widget.notebookViewModeChanged.disconnect(handler);
287
- };
288
- }, [widget]);
289
-
290
314
  const handleNotebookAvailabilityChange = React.useCallback(
291
315
  (available: boolean) => {
292
316
  widget.setHasNotebookView(available);
@@ -294,20 +318,19 @@ function ModelDiffView(props: {
294
318
  [widget]
295
319
  );
296
320
 
297
- // Mirror toolbar-driven diff-style changes into React state.
298
- const [diffStyle, setDiffStyle] = React.useState<DiffStyle>(
299
- () => widget.diffStyle
321
+ const handleSplitRatioChange = React.useCallback(
322
+ (splitRatio: number) => {
323
+ preferences.update({ splitRatio });
324
+ },
325
+ [preferences]
326
+ );
327
+
328
+ const handleImageViewModeChange = React.useCallback(
329
+ (imageViewMode: ImageDiffViewMode) => {
330
+ preferences.update({ imageViewMode });
331
+ },
332
+ [preferences]
300
333
  );
301
- React.useEffect(() => {
302
- const handler = (sender: XtralabDiffWidget, style: DiffStyle): void => {
303
- setDiffStyle(style);
304
- };
305
- widget.diffStyleChanged.connect(handler);
306
- setDiffStyle(widget.diffStyle);
307
- return () => {
308
- widget.diffStyleChanged.disconnect(handler);
309
- };
310
- }, [widget]);
311
334
 
312
335
  const handleFileDiffActiveChange = React.useCallback(
313
336
  (active: boolean) => {
@@ -343,14 +366,12 @@ function ModelDiffView(props: {
343
366
  };
344
367
  }, [themeManager]);
345
368
 
346
- // Image diffs use the server's base64 payloads; other binaries show a placeholder.
347
369
  const isImage = React.useMemo(
348
370
  () => imageDataType(model.filename) !== null,
349
371
  [model.filename]
350
372
  );
351
373
  const isBinary = model.isBinary === true && !isImage;
352
374
 
353
- // Used by the launcher to auto-close an emptied diff after discard.
354
375
  const [hunkCount, setHunkCount] = React.useState<number | null>(null);
355
376
  const handleMetadataChange = React.useCallback(
356
377
  (info: { hunkCount: number | null }) => {
@@ -359,10 +380,8 @@ function ModelDiffView(props: {
359
380
  []
360
381
  );
361
382
 
362
- // Fetch both sides whenever the model or reload nonce changes.
363
383
  React.useEffect(() => {
364
384
  let cancelled = false;
365
- // Non-image binaries do not need content fetches.
366
385
  if (isBinary) {
367
386
  setState({ loading: false, oldText: '', newText: '', error: null });
368
387
  // An in-flight `refresh()` still has to resolve.
@@ -399,7 +418,6 @@ function ModelDiffView(props: {
399
418
  });
400
419
  } finally {
401
420
  if (!cancelled) {
402
- // Let `refresh()` settle once content has loaded.
403
421
  widget.settleRefresh();
404
422
  }
405
423
  }
@@ -422,7 +440,6 @@ function ModelDiffView(props: {
422
440
  }
423
441
  }, [state.loading, state.error, isBinary, hunkCount, nonce, widget]);
424
442
 
425
- // The launcher sets `canDiscard`; jupyterlab-git models fall back to source.
426
443
  const canDiscardHunk =
427
444
  model.canDiscard ??
428
445
  (model.challenger.source === Git.Diff.SpecialRef.WORKING &&
@@ -444,7 +461,6 @@ function ModelDiffView(props: {
444
461
  });
445
462
  },
446
463
  onAfterSave: () => {
447
- // Re-pull so the diff reflects the reverted hunk.
448
464
  void widget.refresh();
449
465
  }
450
466
  }),
@@ -481,8 +497,13 @@ function ModelDiffView(props: {
481
497
  dark={dark}
482
498
  pierreTheme={pierre}
483
499
  rendermime={rendermime}
484
- notebookViewMode={notebookViewMode}
485
- diffStyle={diffStyle}
500
+ notebookViewMode={values.notebookViewMode}
501
+ diffStyle={values.diffStyle}
502
+ lineWrap={values.lineWrap}
503
+ splitRatio={values.splitRatio}
504
+ onSplitRatioChange={handleSplitRatioChange}
505
+ imageViewMode={values.imageViewMode}
506
+ onImageViewModeChange={handleImageViewModeChange}
486
507
  onNotebookAvailabilityChange={handleNotebookAvailabilityChange}
487
508
  onFileDiffActiveChange={handleFileDiffActiveChange}
488
509
  onMetadataChange={handleMetadataChange}
@@ -503,6 +524,9 @@ class NotebookViewModeToolbarItem extends ReactWidget {
503
524
  this.addClass('jp-xtralab-DiffWidget-viewModeToolbarItem');
504
525
  }
505
526
 
527
+ /**
528
+ * Render the Notebook/JSON toggle.
529
+ */
506
530
  protected render(): React.ReactElement {
507
531
  return <NotebookViewModeToolbarControl widget={this._widget} />;
508
532
  }
@@ -514,38 +538,27 @@ function NotebookViewModeToolbarControl(props: {
514
538
  widget: XtralabDiffWidget;
515
539
  }): React.ReactElement {
516
540
  const { widget } = props;
517
- const [mode, setMode] = React.useState<NotebookDiffViewMode>(
518
- () => widget.notebookViewMode
519
- );
541
+ const { preferences, trans } = widget.context;
542
+ const { notebookViewMode } = useDiffPreferences(preferences);
520
543
  const [available, setAvailable] = React.useState<boolean>(
521
544
  () => widget.hasNotebookView
522
545
  );
523
546
  React.useEffect(() => {
524
- const onMode = (
525
- sender: XtralabDiffWidget,
526
- next: NotebookDiffViewMode
527
- ): void => {
528
- setMode(next);
529
- };
530
547
  const onAvailable = (sender: XtralabDiffWidget, next: boolean): void => {
531
548
  setAvailable(next);
532
549
  };
533
- widget.notebookViewModeChanged.connect(onMode);
534
550
  widget.hasNotebookViewChanged.connect(onAvailable);
535
- setMode(widget.notebookViewMode);
536
551
  setAvailable(widget.hasNotebookView);
537
552
  return () => {
538
- widget.notebookViewModeChanged.disconnect(onMode);
539
553
  widget.hasNotebookViewChanged.disconnect(onAvailable);
540
554
  };
541
555
  }, [widget]);
542
556
 
543
- const trans = widget.context.trans;
544
557
  return (
545
558
  <NotebookViewModeControl
546
- mode={mode}
559
+ mode={notebookViewMode}
547
560
  available={available}
548
- onChange={next => widget.setNotebookViewMode(next)}
561
+ onChange={next => preferences.update({ notebookViewMode: next })}
549
562
  trans={trans}
550
563
  />
551
564
  );
@@ -561,6 +574,9 @@ class DiffStyleToolbarItem extends ReactWidget {
561
574
  this.addClass('jp-xtralab-DiffWidget-diffStyleToolbarItem');
562
575
  }
563
576
 
577
+ /**
578
+ * Render the Split/Unified toggle.
579
+ */
564
580
  protected render(): React.ReactElement {
565
581
  return <DiffStyleToolbarControl widget={this._widget} />;
566
582
  }
@@ -572,42 +588,85 @@ function DiffStyleToolbarControl(props: {
572
588
  widget: XtralabDiffWidget;
573
589
  }): React.ReactElement {
574
590
  const { widget } = props;
575
- const [style, setStyle] = React.useState<DiffStyle>(() => widget.diffStyle);
576
- const [available, setAvailable] = React.useState<boolean>(
577
- () => widget.fileDiffActive
591
+ const { preferences, trans } = widget.context;
592
+ const { diffStyle, diffStyleControl } = useDiffPreferences(preferences);
593
+ const available = useFileDiffActive(widget);
594
+ return (
595
+ <DiffStyleControl
596
+ diffStyle={diffStyle}
597
+ control={diffStyleControl}
598
+ available={available}
599
+ onChange={next => preferences.update({ diffStyle: next })}
600
+ trans={trans}
601
+ />
578
602
  );
579
- React.useEffect(() => {
580
- const onStyle = (sender: XtralabDiffWidget, next: DiffStyle): void => {
581
- setStyle(next);
582
- };
583
- const onAvailable = (sender: XtralabDiffWidget, next: boolean): void => {
584
- setAvailable(next);
585
- };
586
- widget.diffStyleChanged.connect(onStyle);
587
- widget.fileDiffActiveChanged.connect(onAvailable);
588
- setStyle(widget.diffStyle);
589
- setAvailable(widget.fileDiffActive);
590
- return () => {
591
- widget.diffStyleChanged.disconnect(onStyle);
592
- widget.fileDiffActiveChanged.disconnect(onAvailable);
593
- };
594
- }, [widget]);
603
+ }
604
+
605
+ /**
606
+ * Line-wrap toggle mounted into a host-owned toolbar.
607
+ */
608
+ class LineWrapToolbarItem extends ReactWidget {
609
+ constructor(widget: XtralabDiffWidget) {
610
+ super();
611
+ this._widget = widget;
612
+ this.addClass('jp-xtralab-DiffWidget-lineWrapToolbarItem');
613
+ }
614
+
615
+ /**
616
+ * Render the line-wrap toggle.
617
+ */
618
+ protected render(): React.ReactElement {
619
+ return <LineWrapToolbarControl widget={this._widget} />;
620
+ }
621
+
622
+ private _widget: XtralabDiffWidget;
623
+ }
595
624
 
596
- const trans = widget.context.trans;
625
+ function LineWrapToolbarControl(props: {
626
+ widget: XtralabDiffWidget;
627
+ }): React.ReactElement {
628
+ const { widget } = props;
629
+ const { preferences, trans } = widget.context;
630
+ const { lineWrap } = useDiffPreferences(preferences);
631
+ const available = useFileDiffActive(widget);
597
632
  return (
598
- <DiffStyleControl
599
- diffStyle={style}
633
+ <LineWrapControl
634
+ lineWrap={lineWrap}
600
635
  available={available}
601
- onChange={next => widget.setDiffStyle(next)}
636
+ onChange={next => preferences.update({ lineWrap: next })}
602
637
  trans={trans}
603
638
  />
604
639
  );
605
640
  }
606
641
 
642
+ /**
643
+ * Track {@link XtralabDiffWidget.fileDiffActive} for the split/unified and
644
+ * line-wrap controls.
645
+ */
646
+ function useFileDiffActive(widget: XtralabDiffWidget): boolean {
647
+ const [active, setActive] = React.useState<boolean>(
648
+ () => widget.fileDiffActive
649
+ );
650
+ React.useEffect(() => {
651
+ const onActive = (sender: XtralabDiffWidget, next: boolean): void => {
652
+ setActive(next);
653
+ };
654
+ widget.fileDiffActiveChanged.connect(onActive);
655
+ setActive(widget.fileDiffActive);
656
+ return () => {
657
+ widget.fileDiffActiveChanged.disconnect(onActive);
658
+ };
659
+ }, [widget]);
660
+ return active;
661
+ }
662
+
607
663
  /**
608
664
  * Structural toolbar type shared across host package boundaries.
609
665
  */
610
666
  interface IDiffToolbar {
667
+ /**
668
+ * Add an item to the toolbar under the given name.
669
+ */
611
670
  addItem(name: string, widget: Widget): boolean;
612
671
  }
613
672
 
@@ -623,4 +682,5 @@ export function addDiffToolbarItems(
623
682
  new NotebookViewModeToolbarItem(widget)
624
683
  );
625
684
  toolbar.addItem('xtralab-diff-style', new DiffStyleToolbarItem(widget));
685
+ toolbar.addItem('xtralab-diff-line-wrap', new LineWrapToolbarItem(widget));
626
686
  }