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
@@ -5,7 +5,8 @@ import { ReactWidget } from '@jupyterlab/ui-components';
5
5
  import { PromiseDelegate } from '@lumino/coreutils';
6
6
  import { Signal } from '@lumino/signaling';
7
7
  import { buildDiffAskRequest } from './askRequest';
8
- import { DIFF_WIDGET_CSS_CLASS, DiffStyleControl, DiffSurface, NotebookViewModeControl, isDarkTheme, isPierreTheme, readStoredDiffStyle, readStoredNotebookViewMode, writeStoredDiffStyle, writeStoredNotebookViewMode } from './diffSurface';
8
+ import { useDiffPreferences } from './diffPreferences';
9
+ import { DIFF_WIDGET_CSS_CLASS, DiffStyleControl, DiffSurface, LineWrapControl, NotebookViewModeControl, isDarkTheme, isPierreTheme } from './diffSurface';
9
10
  import { imageDataType } from './imageDiff';
10
11
  export { DIFF_WIDGET_CSS_CLASS };
11
12
  /**
@@ -16,21 +17,20 @@ export class XtralabDiffWidget extends ReactWidget {
16
17
  super();
17
18
  this._reloadNonce = 0;
18
19
  this._pendingRefresh = null;
19
- this._notebookViewModeChanged = new Signal(this);
20
20
  this._hasNotebookView = false;
21
21
  this._hasNotebookViewChanged = new Signal(this);
22
- this._diffStyleChanged = new Signal(this);
23
22
  this._fileDiffActive = false;
24
23
  this._fileDiffActiveChanged = new Signal(this);
25
24
  this._emptied = new Signal(this);
26
25
  this._model = model;
27
26
  this._context = context;
28
- this._notebookViewMode = readStoredNotebookViewMode();
29
- this._diffStyle = readStoredDiffStyle();
30
27
  // Reuse jupyterlab-git's host sizing while applying xtralab styling.
31
28
  this.addClass('jp-git-diff-root');
32
29
  this.addClass(DIFF_WIDGET_CSS_CLASS);
33
30
  }
31
+ /**
32
+ * The git diff model being rendered.
33
+ */
34
34
  get model() {
35
35
  return this._model;
36
36
  }
@@ -74,23 +74,15 @@ export class XtralabDiffWidget extends ReactWidget {
74
74
  this.update();
75
75
  await done.promise;
76
76
  }
77
- get notebookViewMode() {
78
- return this._notebookViewMode;
79
- }
80
- setNotebookViewMode(mode) {
81
- if (mode === this._notebookViewMode) {
82
- return;
83
- }
84
- this._notebookViewMode = mode;
85
- writeStoredNotebookViewMode(mode);
86
- this._notebookViewModeChanged.emit(mode);
87
- }
88
- get notebookViewModeChanged() {
89
- return this._notebookViewModeChanged;
90
- }
77
+ /**
78
+ * Whether a rendered notebook view is currently available.
79
+ */
91
80
  get hasNotebookView() {
92
81
  return this._hasNotebookView;
93
82
  }
83
+ /**
84
+ * Set whether a rendered notebook view is available.
85
+ */
94
86
  setHasNotebookView(value) {
95
87
  if (value === this._hasNotebookView) {
96
88
  return;
@@ -98,29 +90,21 @@ export class XtralabDiffWidget extends ReactWidget {
98
90
  this._hasNotebookView = value;
99
91
  this._hasNotebookViewChanged.emit(value);
100
92
  }
93
+ /**
94
+ * A signal emitted when rendered-notebook availability changes.
95
+ */
101
96
  get hasNotebookViewChanged() {
102
97
  return this._hasNotebookViewChanged;
103
98
  }
104
- get diffStyle() {
105
- return this._diffStyle;
106
- }
107
- setDiffStyle(style) {
108
- if (style === this._diffStyle) {
109
- return;
110
- }
111
- this._diffStyle = style;
112
- writeStoredDiffStyle(style);
113
- this._diffStyleChanged.emit(style);
114
- }
115
- get diffStyleChanged() {
116
- return this._diffStyleChanged;
117
- }
118
99
  /**
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() {
122
103
  return this._fileDiffActive;
123
104
  }
105
+ /**
106
+ * Set whether the textual file diff is the active view.
107
+ */
124
108
  setFileDiffActive(value) {
125
109
  if (value === this._fileDiffActive) {
126
110
  return;
@@ -128,6 +112,9 @@ export class XtralabDiffWidget extends ReactWidget {
128
112
  this._fileDiffActive = value;
129
113
  this._fileDiffActiveChanged.emit(value);
130
114
  }
115
+ /**
116
+ * A signal emitted when {@link fileDiffActive} changes.
117
+ */
131
118
  get fileDiffActiveChanged() {
132
119
  return this._fileDiffActiveChanged;
133
120
  }
@@ -137,27 +124,44 @@ export class XtralabDiffWidget extends ReactWidget {
137
124
  get emptied() {
138
125
  return this._emptied;
139
126
  }
127
+ /**
128
+ * Emit the {@link emptied} signal.
129
+ */
140
130
  notifyEmptied() {
141
131
  this._emptied.emit();
142
132
  }
133
+ /**
134
+ * Resolve and clear the pending `refresh()` promise, if any.
135
+ */
143
136
  settleRefresh() {
144
137
  const pending = this._pendingRefresh;
145
138
  this._pendingRefresh = null;
146
139
  pending === null || pending === void 0 ? void 0 : pending.resolve();
147
140
  }
141
+ /**
142
+ * Dispose of the resources held by the widget.
143
+ */
148
144
  dispose() {
149
- // Release any in-flight refresh awaiter so a host that awaits refresh()
150
- // (jupyterlab-git re-shows its diff button only once it resolves) does not
151
- // hang when the widget is torn down mid-fetch.
145
+ // jupyterlab-git awaits refresh() to re-show its diff button; release any
146
+ // in-flight awaiter so it does not hang on mid-fetch teardown.
152
147
  this.settleRefresh();
153
148
  super.dispose();
154
149
  }
150
+ /**
151
+ * A counter incremented on each `refresh()` to trigger a content re-fetch.
152
+ */
155
153
  get reloadNonce() {
156
154
  return this._reloadNonce;
157
155
  }
156
+ /**
157
+ * The application-level services used by the rendered diff.
158
+ */
158
159
  get context() {
159
160
  return this._context;
160
161
  }
162
+ /**
163
+ * Render the diff view for the current model.
164
+ */
161
165
  render() {
162
166
  return React.createElement(ModelDiffView, { widget: this });
163
167
  }
@@ -166,7 +170,8 @@ function ModelDiffView(props) {
166
170
  var _a, _b;
167
171
  const { widget } = props;
168
172
  const model = widget.model;
169
- const { contentsManager, rendermime, themeManager, askAgent, trans } = widget.context;
173
+ const { contentsManager, rendermime, themeManager, askAgent, preferences, trans } = widget.context;
174
+ const values = useDiffPreferences(preferences);
170
175
  const nonce = widget.reloadNonce;
171
176
  const [state, setState] = React.useState({
172
177
  loading: true,
@@ -174,33 +179,15 @@ function ModelDiffView(props) {
174
179
  newText: '',
175
180
  error: null
176
181
  });
177
- // Mirror toolbar-driven notebook view changes into React state.
178
- const [notebookViewMode, setNotebookViewMode] = React.useState(() => widget.notebookViewMode);
179
- React.useEffect(() => {
180
- const handler = (sender, mode) => {
181
- setNotebookViewMode(mode);
182
- };
183
- widget.notebookViewModeChanged.connect(handler);
184
- setNotebookViewMode(widget.notebookViewMode);
185
- return () => {
186
- widget.notebookViewModeChanged.disconnect(handler);
187
- };
188
- }, [widget]);
189
182
  const handleNotebookAvailabilityChange = React.useCallback((available) => {
190
183
  widget.setHasNotebookView(available);
191
184
  }, [widget]);
192
- // Mirror toolbar-driven diff-style changes into React state.
193
- const [diffStyle, setDiffStyle] = React.useState(() => widget.diffStyle);
194
- React.useEffect(() => {
195
- const handler = (sender, style) => {
196
- setDiffStyle(style);
197
- };
198
- widget.diffStyleChanged.connect(handler);
199
- setDiffStyle(widget.diffStyle);
200
- return () => {
201
- widget.diffStyleChanged.disconnect(handler);
202
- };
203
- }, [widget]);
185
+ const handleSplitRatioChange = React.useCallback((splitRatio) => {
186
+ preferences.update({ splitRatio });
187
+ }, [preferences]);
188
+ const handleImageViewModeChange = React.useCallback((imageViewMode) => {
189
+ preferences.update({ imageViewMode });
190
+ }, [preferences]);
204
191
  const handleFileDiffActiveChange = React.useCallback((active) => {
205
192
  widget.setFileDiffActive(active);
206
193
  }, [widget]);
@@ -226,18 +213,14 @@ function ModelDiffView(props) {
226
213
  observer.disconnect();
227
214
  };
228
215
  }, [themeManager]);
229
- // Image diffs use the server's base64 payloads; other binaries show a placeholder.
230
216
  const isImage = React.useMemo(() => imageDataType(model.filename) !== null, [model.filename]);
231
217
  const isBinary = model.isBinary === true && !isImage;
232
- // Used by the launcher to auto-close an emptied diff after discard.
233
218
  const [hunkCount, setHunkCount] = React.useState(null);
234
219
  const handleMetadataChange = React.useCallback((info) => {
235
220
  setHunkCount(info.hunkCount);
236
221
  }, []);
237
- // Fetch both sides whenever the model or reload nonce changes.
238
222
  React.useEffect(() => {
239
223
  let cancelled = false;
240
- // Non-image binaries do not need content fetches.
241
224
  if (isBinary) {
242
225
  setState({ loading: false, oldText: '', newText: '', error: null });
243
226
  // An in-flight `refresh()` still has to resolve.
@@ -276,7 +259,6 @@ function ModelDiffView(props) {
276
259
  }
277
260
  finally {
278
261
  if (!cancelled) {
279
- // Let `refresh()` settle once content has loaded.
280
262
  widget.settleRefresh();
281
263
  }
282
264
  }
@@ -295,7 +277,6 @@ function ModelDiffView(props) {
295
277
  widget.notifyEmptied();
296
278
  }
297
279
  }, [state.loading, state.error, isBinary, hunkCount, nonce, widget]);
298
- // The launcher sets `canDiscard`; jupyterlab-git models fall back to source.
299
280
  const canDiscardHunk = (_a = model.canDiscard) !== null && _a !== void 0 ? _a : (model.challenger.source === Git.Diff.SpecialRef.WORKING &&
300
281
  model.hasConflict !== true);
301
282
  const serverPath = React.useMemo(() => { var _a; return PathExt.join((_a = model.repositoryPath) !== null && _a !== void 0 ? _a : '', model.filename); }, [model.repositoryPath, model.filename]);
@@ -309,7 +290,6 @@ function ModelDiffView(props) {
309
290
  });
310
291
  },
311
292
  onAfterSave: () => {
312
- // Re-pull so the diff reflects the reverted hunk.
313
293
  void widget.refresh();
314
294
  }
315
295
  }), [canDiscardHunk, contentsManager, serverPath, widget]);
@@ -328,7 +308,7 @@ function ModelDiffView(props) {
328
308
  }));
329
309
  };
330
310
  }, [askAgent, model, state.oldText, state.newText, trans]);
331
- return (React.createElement(DiffSurface, { loading: state.loading, error: state.error, isBinary: isBinary, oldText: state.oldText, newText: state.newText, newName: model.filename, oldName: (_b = model.oldFilename) !== null && _b !== void 0 ? _b : model.filename, dark: dark, pierreTheme: pierre, rendermime: rendermime, notebookViewMode: notebookViewMode, diffStyle: diffStyle, onNotebookAvailabilityChange: handleNotebookAvailabilityChange, onFileDiffActiveChange: handleFileDiffActiveChange, onMetadataChange: handleMetadataChange, hunkDiscard: hunkDiscard, onLineAsk: handleLineAsk, trans: trans }));
311
+ return (React.createElement(DiffSurface, { loading: state.loading, error: state.error, isBinary: isBinary, oldText: state.oldText, newText: state.newText, newName: model.filename, oldName: (_b = model.oldFilename) !== null && _b !== void 0 ? _b : model.filename, dark: dark, pierreTheme: pierre, rendermime: rendermime, notebookViewMode: values.notebookViewMode, diffStyle: values.diffStyle, lineWrap: values.lineWrap, splitRatio: values.splitRatio, onSplitRatioChange: handleSplitRatioChange, imageViewMode: values.imageViewMode, onImageViewModeChange: handleImageViewModeChange, onNotebookAvailabilityChange: handleNotebookAvailabilityChange, onFileDiffActiveChange: handleFileDiffActiveChange, onMetadataChange: handleMetadataChange, hunkDiscard: hunkDiscard, onLineAsk: handleLineAsk, trans: trans }));
332
312
  }
333
313
  /**
334
314
  * Notebook/JSON toggle mounted into a host-owned toolbar.
@@ -339,32 +319,29 @@ class NotebookViewModeToolbarItem extends ReactWidget {
339
319
  this._widget = widget;
340
320
  this.addClass('jp-xtralab-DiffWidget-viewModeToolbarItem');
341
321
  }
322
+ /**
323
+ * Render the Notebook/JSON toggle.
324
+ */
342
325
  render() {
343
326
  return React.createElement(NotebookViewModeToolbarControl, { widget: this._widget });
344
327
  }
345
328
  }
346
329
  function NotebookViewModeToolbarControl(props) {
347
330
  const { widget } = props;
348
- const [mode, setMode] = React.useState(() => widget.notebookViewMode);
331
+ const { preferences, trans } = widget.context;
332
+ const { notebookViewMode } = useDiffPreferences(preferences);
349
333
  const [available, setAvailable] = React.useState(() => widget.hasNotebookView);
350
334
  React.useEffect(() => {
351
- const onMode = (sender, next) => {
352
- setMode(next);
353
- };
354
335
  const onAvailable = (sender, next) => {
355
336
  setAvailable(next);
356
337
  };
357
- widget.notebookViewModeChanged.connect(onMode);
358
338
  widget.hasNotebookViewChanged.connect(onAvailable);
359
- setMode(widget.notebookViewMode);
360
339
  setAvailable(widget.hasNotebookView);
361
340
  return () => {
362
- widget.notebookViewModeChanged.disconnect(onMode);
363
341
  widget.hasNotebookViewChanged.disconnect(onAvailable);
364
342
  };
365
343
  }, [widget]);
366
- const trans = widget.context.trans;
367
- return (React.createElement(NotebookViewModeControl, { mode: mode, available: available, onChange: next => widget.setNotebookViewMode(next), trans: trans }));
344
+ return (React.createElement(NotebookViewModeControl, { mode: notebookViewMode, available: available, onChange: next => preferences.update({ notebookViewMode: next }), trans: trans }));
368
345
  }
369
346
  /**
370
347
  * Split/Unified toggle mounted into a host-owned toolbar.
@@ -375,32 +352,60 @@ class DiffStyleToolbarItem extends ReactWidget {
375
352
  this._widget = widget;
376
353
  this.addClass('jp-xtralab-DiffWidget-diffStyleToolbarItem');
377
354
  }
355
+ /**
356
+ * Render the Split/Unified toggle.
357
+ */
378
358
  render() {
379
359
  return React.createElement(DiffStyleToolbarControl, { widget: this._widget });
380
360
  }
381
361
  }
382
362
  function DiffStyleToolbarControl(props) {
383
363
  const { widget } = props;
384
- const [style, setStyle] = React.useState(() => widget.diffStyle);
385
- const [available, setAvailable] = React.useState(() => widget.fileDiffActive);
364
+ const { preferences, trans } = widget.context;
365
+ const { diffStyle, diffStyleControl } = useDiffPreferences(preferences);
366
+ const available = useFileDiffActive(widget);
367
+ return (React.createElement(DiffStyleControl, { diffStyle: diffStyle, control: diffStyleControl, available: available, onChange: next => preferences.update({ diffStyle: next }), trans: trans }));
368
+ }
369
+ /**
370
+ * Line-wrap toggle mounted into a host-owned toolbar.
371
+ */
372
+ class LineWrapToolbarItem extends ReactWidget {
373
+ constructor(widget) {
374
+ super();
375
+ this._widget = widget;
376
+ this.addClass('jp-xtralab-DiffWidget-lineWrapToolbarItem');
377
+ }
378
+ /**
379
+ * Render the line-wrap toggle.
380
+ */
381
+ render() {
382
+ return React.createElement(LineWrapToolbarControl, { widget: this._widget });
383
+ }
384
+ }
385
+ function LineWrapToolbarControl(props) {
386
+ const { widget } = props;
387
+ const { preferences, trans } = widget.context;
388
+ const { lineWrap } = useDiffPreferences(preferences);
389
+ const available = useFileDiffActive(widget);
390
+ return (React.createElement(LineWrapControl, { lineWrap: lineWrap, available: available, onChange: next => preferences.update({ lineWrap: next }), trans: trans }));
391
+ }
392
+ /**
393
+ * Track {@link XtralabDiffWidget.fileDiffActive} for the split/unified and
394
+ * line-wrap controls.
395
+ */
396
+ function useFileDiffActive(widget) {
397
+ const [active, setActive] = React.useState(() => widget.fileDiffActive);
386
398
  React.useEffect(() => {
387
- const onStyle = (sender, next) => {
388
- setStyle(next);
389
- };
390
- const onAvailable = (sender, next) => {
391
- setAvailable(next);
399
+ const onActive = (sender, next) => {
400
+ setActive(next);
392
401
  };
393
- widget.diffStyleChanged.connect(onStyle);
394
- widget.fileDiffActiveChanged.connect(onAvailable);
395
- setStyle(widget.diffStyle);
396
- setAvailable(widget.fileDiffActive);
402
+ widget.fileDiffActiveChanged.connect(onActive);
403
+ setActive(widget.fileDiffActive);
397
404
  return () => {
398
- widget.diffStyleChanged.disconnect(onStyle);
399
- widget.fileDiffActiveChanged.disconnect(onAvailable);
405
+ widget.fileDiffActiveChanged.disconnect(onActive);
400
406
  };
401
407
  }, [widget]);
402
- const trans = widget.context.trans;
403
- return (React.createElement(DiffStyleControl, { diffStyle: style, available: available, onChange: next => widget.setDiffStyle(next), trans: trans }));
408
+ return active;
404
409
  }
405
410
  /**
406
411
  * Mount the shared diff toolbar controls into a host-owned toolbar.
@@ -408,4 +413,5 @@ function DiffStyleToolbarControl(props) {
408
413
  export function addDiffToolbarItems(toolbar, widget) {
409
414
  toolbar.addItem('xtralab-notebook-view-mode', new NotebookViewModeToolbarItem(widget));
410
415
  toolbar.addItem('xtralab-diff-style', new DiffStyleToolbarItem(widget));
416
+ toolbar.addItem('xtralab-diff-line-wrap', new LineWrapToolbarItem(widget));
411
417
  }
@@ -1,14 +1,15 @@
1
1
  import * as React from 'react';
2
2
  import type { TranslationBundle } from '@jupyterlab/translation';
3
- /**
4
- * The file extensions registered as the xtralab image diff provider.
5
- */
3
+ import type { ImageDiffViewMode } from './diffPreferences';
6
4
  export declare const IMAGE_DIFF_EXTENSIONS: string[];
7
5
  /**
8
6
  * The `data:` MIME subtype for a path xtralab renders as an image, or
9
7
  * `null` when the path is not a supported raster image.
10
8
  */
11
9
  export declare function imageDataType(path: string): string | null;
10
+ /**
11
+ * Props for {@link ImageDiffView}.
12
+ */
12
13
  interface IImageDiffViewProps {
13
14
  /**
14
15
  * Base64 of the reference (old) revision; empty when added.
@@ -22,15 +23,22 @@ interface IImageDiffViewProps {
22
23
  * MIME subtype from {@link imageDataType}.
23
24
  */
24
25
  fileType: string;
26
+ /**
27
+ * The selected comparison mode (host-controlled).
28
+ */
29
+ mode: ImageDiffViewMode;
30
+ /**
31
+ * Called when the user picks another comparison mode.
32
+ */
33
+ onModeChange: (mode: ImageDiffViewMode) => void;
25
34
  /**
26
35
  * Translation bundle for user-facing strings.
27
36
  */
28
37
  trans: TranslationBundle;
29
38
  }
30
39
  /**
31
- * The image comparison surface: a segmented mode selector (reusing the
32
- * diff segmented-control styling so it matches the Notebook/JSON toggle)
33
- * plus the selected 2-up / swipe / onion-skin view.
40
+ * The image comparison surface: a mode selector (reusing the diff widget's
41
+ * segmented-control styling) plus the selected 2-up / swipe / onion view.
34
42
  */
35
43
  export declare function ImageDiffView(props: IImageDiffViewProps): React.ReactElement;
36
44
  export {};
@@ -1,20 +1,14 @@
1
1
  import * as React from 'react';
2
2
  /**
3
3
  * xtralab's image diff, registered in place of `@jupyterlab/git:image-diff`.
4
- *
5
- * The git server base64-encodes binary content, so both diff hosts receive
6
- * the two sides as base64 strings through the same `content()` getters used
7
- * for text. This module turns those strings into `<img>` elements and
8
- * offers the 2-up / swipe / onion-skin comparison modes; no diffing library
9
- * is involved (`@pierre/diffs` is text-only).
4
+ * Both sides arrive base64-encoded through the same `content()` getters used
5
+ * for text; the 2-up/swipe/onion views are hand-rolled (`@pierre/diffs` is
6
+ * text-only).
10
7
  */
11
8
  /**
12
- * Raster image extensions xtralab renders as an image diff, mapped to the
13
- * MIME subtype used in the `data:image/<subtype>;base64,…` URL.
14
- *
15
- * `.svg` is intentionally absent: it is XML text, so the `@pierre/diffs`
16
- * text diff (the fallback provider) is more useful for it than an image
17
- * comparison.
9
+ * Raster extensions mapped to the `data:image/<subtype>` MIME subtype.
10
+ * `.svg` is intentionally absent: it is XML text, so the fallback text diff
11
+ * is more useful than an image comparison.
18
12
  */
19
13
  const IMAGE_DATA_TYPES = {
20
14
  '.png': 'png',
@@ -25,9 +19,6 @@ const IMAGE_DATA_TYPES = {
25
19
  '.bmp': 'bmp',
26
20
  '.ico': 'x-icon'
27
21
  };
28
- /**
29
- * The file extensions registered as the xtralab image diff provider.
30
- */
31
22
  export const IMAGE_DIFF_EXTENSIONS = Object.keys(IMAGE_DATA_TYPES);
32
23
  /**
33
24
  * The `data:` MIME subtype for a path xtralab renders as an image, or
@@ -42,40 +33,14 @@ export function imageDataType(path) {
42
33
  }
43
34
  return (_a = IMAGE_DATA_TYPES[lower.slice(dot)]) !== null && _a !== void 0 ? _a : null;
44
35
  }
45
- const IMAGE_DIFF_VIEW_MODE_STORAGE_KEY = 'xtralab:image-diff-view-mode';
46
- function readStoredImageViewMode() {
47
- try {
48
- const raw = window.localStorage.getItem(IMAGE_DIFF_VIEW_MODE_STORAGE_KEY);
49
- if (raw === '2-up' || raw === 'swipe' || raw === 'onion') {
50
- return raw;
51
- }
52
- }
53
- catch (_a) {
54
- // localStorage can throw in privacy/sandboxed contexts — 2-up is the
55
- // sensible default anyway.
56
- }
57
- return '2-up';
58
- }
59
- function writeStoredImageViewMode(mode) {
60
- try {
61
- window.localStorage.setItem(IMAGE_DIFF_VIEW_MODE_STORAGE_KEY, mode);
62
- }
63
- catch (_a) {
64
- // Best-effort.
65
- }
66
- }
67
36
  /**
68
- * Strip whitespace from a base64 payload. Python's `base64.encodebytes`
69
- * (used by the git server) inserts a newline every 76 characters; most
70
- * browsers tolerate that inside a `data:` URL but some are stricter, and
71
- * the cleaned length also lets us size the file accurately.
37
+ * Strip whitespace from a base64 payload: the server's `base64.encodebytes`
38
+ * inserts newlines some browsers reject in `data:` URLs, and the cleaned
39
+ * length sizes the file accurately.
72
40
  */
73
41
  function cleanBase64(value) {
74
42
  return value.replace(/\s+/g, '');
75
43
  }
76
- /**
77
- * Decoded byte length of a (whitespace-free) base64 string.
78
- */
79
44
  function base64ByteLength(clean) {
80
45
  if (clean.length === 0) {
81
46
  return 0;
@@ -107,26 +72,20 @@ function toSide(raw, fileType) {
107
72
  };
108
73
  }
109
74
  /**
110
- * The image comparison surface: a segmented mode selector (reusing the
111
- * diff segmented-control styling so it matches the Notebook/JSON toggle)
112
- * plus the selected 2-up / swipe / onion-skin view.
75
+ * The image comparison surface: a mode selector (reusing the diff widget's
76
+ * segmented-control styling) plus the selected 2-up / swipe / onion view.
113
77
  */
114
78
  export function ImageDiffView(props) {
115
- const { reference, challenger, fileType, trans } = props;
79
+ const { reference, challenger, fileType, mode, onModeChange, trans } = props;
116
80
  const ref = React.useMemo(() => toSide(reference, fileType), [reference, fileType]);
117
81
  const chall = React.useMemo(() => toSide(challenger, fileType), [challenger, fileType]);
118
- const [mode, setMode] = React.useState(() => readStoredImageViewMode());
119
- const changeMode = React.useCallback((next) => {
120
- setMode(next);
121
- writeStoredImageViewMode(next);
122
- }, []);
123
82
  return (React.createElement("div", { className: "jp-xtralab-ImageDiff" },
124
83
  React.createElement("div", { className: "jp-xtralab-ImageDiff-header" },
125
84
  React.createElement("div", { className: "jp-xtralab-DiffWidget-segmented", role: "tablist", "aria-label": trans.__('Image diff view mode') }, [
126
85
  ['2-up', trans.__('2-up')],
127
86
  ['swipe', trans.__('Swipe')],
128
87
  ['onion', trans.__('Onion Skin')]
129
- ].map(([value, label]) => (React.createElement("button", { key: value, type: "button", role: "tab", "aria-selected": mode === value, "data-active": mode === value, className: "jp-xtralab-DiffWidget-segmentedButton", onClick: () => changeMode(value) }, label))))),
88
+ ].map(([value, label]) => (React.createElement("button", { key: value, type: "button", role: "tab", "aria-selected": mode === value, "data-active": mode === value, className: "jp-xtralab-DiffWidget-segmentedButton", onClick: () => onModeChange(value) }, label))))),
130
89
  React.createElement("div", { className: "jp-xtralab-ImageDiff-body" }, mode === '2-up' ? (React.createElement(TwoUp, { reference: ref, challenger: chall, trans: trans })) : mode === 'swipe' ? (React.createElement(Swipe, { reference: ref, challenger: chall, trans: trans })) : (React.createElement(OnionSkin, { reference: ref, challenger: chall, trans: trans })))));
131
90
  }
132
91
  function useNaturalSize() {
@@ -157,21 +116,14 @@ function TwoUp({ reference, challenger, trans }) {
157
116
  React.createElement("div", { className: "jp-xtralab-ImageDiff-frame", "data-side": "challenger" }, challenger.uri !== null ? (React.createElement("img", { className: "jp-xtralab-ImageDiff-img", src: challenger.uri, alt: trans.__('Challenger revision'), onLoad: onChallLoad })) : (React.createElement("div", { className: "jp-xtralab-ImageDiff-empty" }, trans.__('Deleted')))),
158
117
  React.createElement("figcaption", { className: "jp-xtralab-ImageDiff-caption" }, caption(challenger, challSize, trans.__('Deleted'))))));
159
118
  }
160
- /**
161
- * Range slider shared by the swipe and onion-skin views. A native
162
- * `<input type="range">` (styled via CSS) keeps the bundle free of a UI
163
- * component dependency.
164
- */
165
119
  function RangeSlider(props) {
166
120
  const { value, onChange, ariaLabel } = props;
167
121
  return (React.createElement("input", { className: "jp-xtralab-ImageDiff-slider", type: "range", min: 0, max: 100, step: 1, value: value, "aria-label": ariaLabel, onChange: event => onChange(Number(event.target.value)) }));
168
122
  }
169
123
  /**
170
- * Fit a `naturalWidth × naturalHeight` image inside an `areaWidth ×
171
- * areaHeight` box without upscaling past its intrinsic size — the same
172
- * "contain, never enlarge" rule the 2-up and onion views get for free from
173
- * `max-width/height: 100%; width/height: auto`. Returns the displayed pixel
174
- * box, or `null` until both the image and the area have been measured.
124
+ * Fit an image inside an area without upscaling past its intrinsic size —
125
+ * the "contain, never enlarge" rule the other views get from CSS. Returns
126
+ * the displayed pixel box, or `null` until both sizes are measured.
175
127
  */
176
128
  function containBox(naturalWidth, naturalHeight, areaWidth, areaHeight) {
177
129
  if (naturalWidth <= 0 ||
@@ -183,9 +135,6 @@ function containBox(naturalWidth, naturalHeight, areaWidth, areaHeight) {
183
135
  const scale = Math.min(areaWidth / naturalWidth, areaHeight / naturalHeight, 1);
184
136
  return { width: naturalWidth * scale, height: naturalHeight * scale };
185
137
  }
186
- /**
187
- * Track an element's content-box size, kept current across layout changes.
188
- */
189
138
  function useElementSize() {
190
139
  const ref = React.useRef(null);
191
140
  const [size, setSize] = React.useState({ width: 0, height: 0 });
@@ -214,13 +163,9 @@ function Swipe({ reference, challenger, trans }) {
214
163
  const [challSize, onChallLoad] = useNaturalSize();
215
164
  const [areaRef, area] = useElementSize();
216
165
  const frameRef = React.useRef(null);
217
- // Display both revisions in a single box — the union of their intrinsic
218
- // sizes, scaled down to fit but never enlarged — and size the frame to
219
- // exactly that box. The divider's `left: N%`, the pointer-to-percent
220
- // mapping (below) and each image's `clip-path` inset then all resolve
221
- // against the same width, so the white line sits precisely on the seam
222
- // between the two revisions at every position. A full-width frame instead
223
- // let the line drift off the seam by as much as half the letterbox.
166
+ // Size the frame to the union box of both revisions so the divider %, the
167
+ // pointer mapping and the clip-paths all resolve against the same width;
168
+ // a full-width frame lets the line drift off the seam.
224
169
  const naturalWidth = Math.max((_a = refSize === null || refSize === void 0 ? void 0 : refSize[0]) !== null && _a !== void 0 ? _a : 0, (_b = challSize === null || challSize === void 0 ? void 0 : challSize[0]) !== null && _b !== void 0 ? _b : 0);
225
170
  const naturalHeight = Math.max((_c = refSize === null || refSize === void 0 ? void 0 : refSize[1]) !== null && _c !== void 0 ? _c : 0, (_d = challSize === null || challSize === void 0 ? void 0 : challSize[1]) !== null && _d !== void 0 ? _d : 0);
226
171
  const box = containBox(naturalWidth, naturalHeight, area.width, area.height);
@@ -237,7 +182,6 @@ function Swipe({ reference, challenger, trans }) {
237
182
  setPosition(Math.max(0, Math.min(100, next)));
238
183
  }, []);
239
184
  const onPointerDown = React.useCallback((event) => {
240
- // Only react to the primary pointer (left click / touch / pen tip).
241
185
  if (event.button !== 0) {
242
186
  return;
243
187
  }
@@ -1,8 +1,5 @@
1
1
  import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
2
  import { CommandArguments, CommandIDs } from './commands';
3
- /**
4
- * Plugins contributed by xtralab's git integration.
5
- */
6
3
  declare const plugins: JupyterFrontEndPlugin<unknown>[];
7
4
  export { CommandArguments, CommandIDs };
8
5
  export default plugins;