xtralab 0.1.1 → 0.2.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 (65) hide show
  1. package/README.md +86 -0
  2. package/lib/fileBrowser/fileBrowser.js +71 -1
  3. package/lib/fileBrowser/gitignore.d.ts +29 -0
  4. package/lib/fileBrowser/gitignore.js +74 -0
  5. package/lib/fileBrowser/index.js +1 -1
  6. package/lib/git/api.d.ts +11 -0
  7. package/lib/git/api.js +19 -0
  8. package/lib/git/commands.d.ts +3 -1
  9. package/lib/git/commands.js +10 -4
  10. package/lib/git/diffWidget.d.ts +11 -0
  11. package/lib/git/diffWidget.js +90 -9
  12. package/lib/git/index.js +23 -7
  13. package/lib/git/panelComponent.d.ts +3 -1
  14. package/lib/git/panelComponent.js +6 -3
  15. package/lib/index.js +9 -1
  16. package/lib/launcher/agents.d.ts +78 -0
  17. package/lib/launcher/agents.js +168 -0
  18. package/lib/launcher/availability.d.ts +12 -0
  19. package/lib/launcher/availability.js +53 -0
  20. package/lib/launcher/commands.d.ts +22 -0
  21. package/lib/launcher/commands.js +89 -0
  22. package/lib/launcher/dashboard.d.ts +46 -0
  23. package/lib/launcher/dashboard.js +164 -0
  24. package/lib/launcher/icons.d.ts +33 -0
  25. package/lib/launcher/icons.js +113 -0
  26. package/lib/launcher/index.d.ts +23 -0
  27. package/lib/launcher/index.js +191 -0
  28. package/lib/launcher/invocation.d.ts +29 -0
  29. package/lib/launcher/invocation.js +42 -0
  30. package/lib/menus/index.d.ts +14 -0
  31. package/lib/menus/index.js +102 -0
  32. package/lib/sidebar/index.d.ts +18 -0
  33. package/lib/sidebar/index.js +155 -0
  34. package/lib/statusBar/index.d.ts +14 -0
  35. package/lib/statusBar/index.js +89 -0
  36. package/lib/statusBar/widget.d.ts +121 -0
  37. package/lib/statusBar/widget.js +237 -0
  38. package/package.json +10 -2
  39. package/schema/launcher.json +70 -0
  40. package/schema/sidebar.json +57 -0
  41. package/src/fileBrowser/fileBrowser.tsx +75 -1
  42. package/src/fileBrowser/gitignore.ts +86 -0
  43. package/src/fileBrowser/index.ts +1 -1
  44. package/src/git/api.ts +20 -0
  45. package/src/git/commands.ts +19 -5
  46. package/src/git/diffWidget.tsx +109 -13
  47. package/src/git/index.ts +32 -6
  48. package/src/git/panelComponent.tsx +17 -5
  49. package/src/index.ts +9 -1
  50. package/src/launcher/agents.ts +248 -0
  51. package/src/launcher/availability.ts +73 -0
  52. package/src/launcher/commands.ts +103 -0
  53. package/src/launcher/dashboard.tsx +356 -0
  54. package/src/launcher/icons.ts +123 -0
  55. package/src/launcher/index.ts +228 -0
  56. package/src/launcher/invocation.ts +45 -0
  57. package/src/menus/index.ts +124 -0
  58. package/src/sidebar/index.ts +195 -0
  59. package/src/statusBar/index.ts +115 -0
  60. package/src/statusBar/widget.tsx +318 -0
  61. package/style/git.css +4 -8
  62. package/style/index.css +2 -0
  63. package/style/index.js +2 -0
  64. package/style/launcher.css +323 -0
  65. package/style/statusBar.css +95 -0
@@ -0,0 +1,86 @@
1
+ import ignore from 'ignore';
2
+ import type { Ignore } from 'ignore';
3
+
4
+ import { Contents } from '@jupyterlab/services';
5
+
6
+ import type { GitStatusEntry } from '@pierre/trees';
7
+
8
+ /**
9
+ * The path of the workspace `.gitignore` file. Loaded relative to the
10
+ * Jupyter contents root, which matches the directory the gitignore patterns
11
+ * are evaluated against — patterns and tested paths share the same base.
12
+ */
13
+ const GITIGNORE_PATH = '.gitignore';
14
+
15
+ /**
16
+ * Load the workspace `.gitignore` and return an `ignore` matcher built from
17
+ * its contents. Returns `null` when the file is missing, empty, or fails to
18
+ * load — callers should treat any of those as "no ignored entries".
19
+ */
20
+ export async function loadGitignoreMatcher(
21
+ contentsManager: Contents.IManager
22
+ ): Promise<Ignore | null> {
23
+ let model: Contents.IModel;
24
+ try {
25
+ model = await contentsManager.get(GITIGNORE_PATH, {
26
+ content: true,
27
+ format: 'text',
28
+ type: 'file'
29
+ });
30
+ } catch {
31
+ // The most common failure here is `.gitignore` simply not existing.
32
+ // Other transient errors (permission denied, server unreachable) are
33
+ // also treated as "no matcher" so the file browser still works.
34
+ return null;
35
+ }
36
+ if (model.type !== 'file' || typeof model.content !== 'string') {
37
+ return null;
38
+ }
39
+ if (model.content.length === 0) {
40
+ return null;
41
+ }
42
+ return ignore().add(model.content);
43
+ }
44
+
45
+ /**
46
+ * Build `GitStatusEntry`s for the canonical paths that match the given
47
+ * gitignore matcher.
48
+ *
49
+ * `@pierre/trees` propagates the `ignored` style from a directory entry
50
+ * down to every descendant via its internal `ignoredDirectoryPaths` set, so
51
+ * we only emit one entry per ignored subtree root: emitting descendants
52
+ * would also pollute the library's `directoriesWithChanges` set, causing
53
+ * parent directories of nested ignored items to render a "contains git
54
+ * status items" dot.
55
+ *
56
+ * Paths are sorted lexicographically so a directory always precedes its
57
+ * descendants, allowing a simple ancestor-suppression check via
58
+ * `path.startsWith(dir)`. The trailing slash on canonical directory paths
59
+ * (required by `@pierre/trees`, also recognized by `ignore` as the
60
+ * "directory" indicator) keeps the prefix check from accidentally matching
61
+ * sibling paths that happen to share a name prefix (e.g. `node_modules_old`
62
+ * vs `node_modules/`).
63
+ */
64
+ export function buildIgnoredEntries(
65
+ matcher: Ignore,
66
+ canonicalPaths: Iterable<string>
67
+ ): GitStatusEntry[] {
68
+ const sorted = Array.from(canonicalPaths).sort();
69
+ const emittedIgnoredDirs: string[] = [];
70
+ const entries: GitStatusEntry[] = [];
71
+ for (const path of sorted) {
72
+ if (path.length === 0) {
73
+ continue;
74
+ }
75
+ if (emittedIgnoredDirs.some(dir => path.startsWith(dir))) {
76
+ continue;
77
+ }
78
+ if (matcher.ignores(path)) {
79
+ entries.push({ path, status: 'ignored' });
80
+ if (path.endsWith('/')) {
81
+ emittedIgnoredDirs.push(path);
82
+ }
83
+ }
84
+ }
85
+ return entries;
86
+ }
@@ -37,7 +37,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
37
37
  void app.commands.execute('docmanager:open', { path: serverPath });
38
38
  }
39
39
  });
40
- app.shell.add(browser, 'left', { rank: 700 });
40
+ app.shell.add(browser, 'left', { rank: 50 });
41
41
  if (restorer) {
42
42
  restorer.add(browser, browser.id);
43
43
  }
package/src/git/api.ts CHANGED
@@ -145,6 +145,26 @@ export async function checkout(
145
145
  });
146
146
  }
147
147
 
148
+ /**
149
+ * Expand the porcelain `files` array into one entry per logical change.
150
+ * A single porcelain row may map to two `IFileChange` rows (a file that's
151
+ * both staged and modified again in the worktree); callers that just want
152
+ * a flat list of all changes — without any staged-vs-unstaged grouping —
153
+ * can use this helper.
154
+ *
155
+ * Provided as a manual concat rather than `Array.prototype.flatMap` so we
156
+ * don't pin our `tsconfig.lib` to ES2019.
157
+ */
158
+ export function expandStatusFiles(files: IGitStatusFile[]): IFileChange[] {
159
+ const result: IFileChange[] = [];
160
+ for (const file of files) {
161
+ for (const change of porcelainToFileChanges(file)) {
162
+ result.push(change);
163
+ }
164
+ }
165
+ return result;
166
+ }
167
+
148
168
  /**
149
169
  * Translate one entry in the porcelain `files` array into the panel's
150
170
  * preferred shape. A single porcelain entry may represent up to two changes
@@ -5,7 +5,11 @@ import { Contents } from '@jupyterlab/services';
5
5
  import { refreshIcon } from '@jupyterlab/ui-components';
6
6
  import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
7
7
 
8
- import { createGitDiffWidget, diffWidgetId } from './diffWidget';
8
+ import {
9
+ createGitDiffWidget,
10
+ diffWidgetId,
11
+ updateGitDiffWidget
12
+ } from './diffWidget';
9
13
  import { GitPanel } from './panel';
10
14
  import { IFileChange } from './tokens';
11
15
 
@@ -29,6 +33,7 @@ export namespace CommandArguments {
29
33
  export interface IOpenDiff {
30
34
  repoPath: string;
31
35
  change: IFileChange;
36
+ pin?: boolean;
32
37
  }
33
38
  }
34
39
 
@@ -65,8 +70,10 @@ export interface IRegisterGitCommandsOptions {
65
70
  * `undefined` when none is open.
66
71
  */
67
72
  findDiff(
68
- change: IFileChange
73
+ change: IFileChange,
74
+ pin?: boolean
69
75
  ): ReturnType<typeof createGitDiffWidget> | undefined;
76
+ onPinned(widget: ReturnType<typeof createGitDiffWidget>): void;
70
77
  }
71
78
 
72
79
  /**
@@ -86,7 +93,8 @@ export function registerGitCommands(
86
93
  rendermime,
87
94
  onChanged,
88
95
  trackDiff,
89
- findDiff
96
+ findDiff,
97
+ onPinned
90
98
  } = options;
91
99
  const { commands } = app;
92
100
 
@@ -107,8 +115,12 @@ export function registerGitCommands(
107
115
  if (typed?.change === undefined) {
108
116
  return;
109
117
  }
110
- const existing = findDiff(typed.change);
118
+ const pin = typed.pin === true;
119
+ const existing = findDiff(typed.change, pin);
111
120
  if (existing !== undefined && !existing.isDisposed) {
121
+ if (!pin) {
122
+ updateGitDiffWidget(existing, typed.change);
123
+ }
112
124
  app.shell.activateById(existing.id);
113
125
  return;
114
126
  }
@@ -118,7 +130,9 @@ export function registerGitCommands(
118
130
  themeManager,
119
131
  contentsManager,
120
132
  rendermime,
121
- onChanged
133
+ onChanged,
134
+ pinned: pin,
135
+ onPinned
122
136
  });
123
137
  await trackDiff(widget);
124
138
  app.shell.add(widget, 'main', { mode: 'tab-after' });
@@ -4,7 +4,12 @@ import { IThemeManager, MainAreaWidget } from '@jupyterlab/apputils';
4
4
  import { PathExt } from '@jupyterlab/coreutils';
5
5
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
6
6
  import { Contents } from '@jupyterlab/services';
7
- import { ReactWidget, undoIcon } from '@jupyterlab/ui-components';
7
+ import {
8
+ ReactWidget,
9
+ ToolbarButton,
10
+ launchIcon,
11
+ undoIcon
12
+ } from '@jupyterlab/ui-components';
8
13
  import { ISignal, Signal } from '@lumino/signaling';
9
14
  import { FileDiff } from '@pierre/diffs/react';
10
15
  import {
@@ -30,6 +35,12 @@ import { GitReference, IFileChange } from './tokens';
30
35
  */
31
36
  export const DIFF_WIDGET_CSS_CLASS = 'jp-xtralab-DiffWidget';
32
37
 
38
+ export const PREVIEW_DIFF_WIDGET_ID = 'xtralab:diff:preview';
39
+
40
+ export function pinnedDiffWidgetId(change: IFileChange): string {
41
+ return `xtralab:diff:pinned:${change.group}:${change.path}`;
42
+ }
43
+
33
44
  /**
34
45
  * `localStorage` key for the user's last-chosen split ratio. Persisted so
35
46
  * the preference survives reloads — the library has no built-in way to
@@ -143,6 +154,10 @@ export function diffWidgetId(change: IFileChange): string {
143
154
  return `xtralab:diff:${change.group}:${change.path}`;
144
155
  }
145
156
 
157
+ function toServerPath(repoPath: string, filePath: string): string {
158
+ return repoPath.length > 0 ? PathExt.join(repoPath, filePath) : filePath;
159
+ }
160
+
146
161
  /**
147
162
  * Resolve the pair of git references whose `content` should appear on the
148
163
  * left and right side of the diff for a given file change. Mirrors VS
@@ -190,6 +205,8 @@ export interface IGitDiffWidgetOptions {
190
205
  * would just be visual noise.
191
206
  */
192
207
  onEmpty?: () => void;
208
+ onPinned?: (widget: MainAreaWidget<DiffContentWidget>) => void;
209
+ pinned?: boolean;
193
210
  }
194
211
 
195
212
  /**
@@ -216,44 +233,88 @@ export function createGitDiffWidget(
216
233
  }
217
234
  });
218
235
  widget = new MainAreaWidget<DiffContentWidget>({ content });
219
- widget.id = diffWidgetId(options.change);
236
+ widget.id =
237
+ options.pinned === true
238
+ ? pinnedDiffWidgetId(options.change)
239
+ : PREVIEW_DIFF_WIDGET_ID;
220
240
  widget.title.label = formatTitle(options.change);
221
241
  widget.title.caption = options.change.path;
222
242
  widget.title.closable = true;
223
243
  widget.title.icon = getTreeIcon(options.change.path);
244
+ widget.title.className = options.pinned === true ? '' : 'jp-mod-preview';
245
+ const onPinned = options.onPinned ?? (() => {});
224
246
  widget.addClass(DIFF_WIDGET_CSS_CLASS);
225
247
  // Wire the notebook view-mode selector into the `MainAreaWidget` toolbar
226
248
  // — the proper home for document chrome controls. The toolbar starts
227
249
  // hidden and becomes visible once the React content reports a parsed
228
250
  // notebook diff via `setHasNotebookView`. Plain text / code diffs never
229
- // flip that flag, so their toolbar bar stays collapsed and the
230
- // resulting layout matches what other read-only main-area documents
231
- // look like in JupyterLab.
251
+ // flip that flag, and pin state can hide the toolbar again, so the host
252
+ // chrome tracks the active mode.
253
+ const pinButton = new ToolbarButton({
254
+ icon: launchIcon,
255
+ tooltip: 'Pin tab',
256
+ onClick: () => content.pin()
257
+ });
258
+ if (content.pinned) {
259
+ pinButton.hide();
260
+ }
261
+ widget.toolbar.addItem('pin', pinButton);
232
262
  widget.toolbar.addItem(
233
263
  'notebook-view-mode',
234
264
  new NotebookViewModeToolbarItem(content)
235
265
  );
236
- widget.toolbar.hide();
237
- const onHasNotebookViewChanged = (
238
- _sender: DiffContentWidget,
239
- value: boolean
240
- ): void => {
266
+ const syncToolbarVisibility = (): void => {
241
267
  if (widget === null || widget.isDisposed) {
242
268
  return;
243
269
  }
244
- if (value) {
270
+ if (!content.pinned || content.hasNotebookView) {
245
271
  widget.toolbar.show();
246
272
  } else {
247
273
  widget.toolbar.hide();
248
274
  }
249
275
  };
276
+ const onHasNotebookViewChanged = (): void => {
277
+ if (widget === null || widget.isDisposed) {
278
+ return;
279
+ }
280
+ syncToolbarVisibility();
281
+ };
282
+ const onPinnedChanged = (sender: DiffContentWidget, value: boolean): void => {
283
+ if (widget === null || widget.isDisposed) {
284
+ return;
285
+ }
286
+ void sender;
287
+ if (value) {
288
+ pinButton.hide();
289
+ // Keep toolbar state and title styling consistent for promoted tabs.
290
+ onPinned(widget);
291
+ if (widget.isDisposed) {
292
+ return;
293
+ }
294
+ widget.title.className = '';
295
+ }
296
+ syncToolbarVisibility();
297
+ };
250
298
  content.hasNotebookViewChanged.connect(onHasNotebookViewChanged);
299
+ content.pinnedChanged.connect(onPinnedChanged);
300
+ syncToolbarVisibility();
251
301
  widget.disposed.connect(() => {
252
302
  content.hasNotebookViewChanged.disconnect(onHasNotebookViewChanged);
303
+ content.pinnedChanged.disconnect(onPinnedChanged);
253
304
  });
254
305
  return widget;
255
306
  }
256
307
 
308
+ export function updateGitDiffWidget(
309
+ widget: MainAreaWidget<DiffContentWidget>,
310
+ change: IFileChange
311
+ ): void {
312
+ widget.content.setChange(change);
313
+ widget.title.label = formatTitle(change);
314
+ widget.title.caption = change.path;
315
+ widget.title.icon = getTreeIcon(change.path);
316
+ }
317
+
257
318
  /**
258
319
  * The actual React-rendered region of the diff viewer. Held in its own
259
320
  * `ReactWidget` so the surrounding `MainAreaWidget` can manage chrome
@@ -270,6 +331,7 @@ export class DiffContentWidget extends ReactWidget {
270
331
  constructor(options: IGitDiffWidgetOptions) {
271
332
  super();
272
333
  this._options = options;
334
+ this._pinned = options.pinned === true;
273
335
  this._notebookViewMode = readStoredNotebookViewMode();
274
336
  }
275
337
 
@@ -320,6 +382,27 @@ export class DiffContentWidget extends ReactWidget {
320
382
  return this._hasNotebookViewChanged;
321
383
  }
322
384
 
385
+ get pinned(): boolean {
386
+ return this._pinned;
387
+ }
388
+
389
+ pin(): void {
390
+ if (this._pinned) {
391
+ return;
392
+ }
393
+ this._pinned = true;
394
+ this._pinnedChanged.emit(true);
395
+ }
396
+
397
+ get pinnedChanged(): ISignal<this, boolean> {
398
+ return this._pinnedChanged;
399
+ }
400
+
401
+ setChange(change: IFileChange): void {
402
+ this._options = { ...this._options, change };
403
+ this.update();
404
+ }
405
+
323
406
  protected render(): React.ReactElement {
324
407
  return <DiffViewer {...this._options} widget={this} />;
325
408
  }
@@ -331,6 +414,8 @@ export class DiffContentWidget extends ReactWidget {
331
414
  );
332
415
  private _hasNotebookView = false;
333
416
  private _hasNotebookViewChanged = new Signal<this, boolean>(this);
417
+ private _pinned: boolean;
418
+ private _pinnedChanged = new Signal<this, boolean>(this);
334
419
  }
335
420
 
336
421
  interface IDiffState {
@@ -505,9 +590,16 @@ function DiffViewer(
505
590
 
506
591
  React.useEffect(() => {
507
592
  let cancelled = false;
593
+ const serverPath = toServerPath(repoPath, change.path);
594
+ setState({
595
+ loading: true,
596
+ metadata: null,
597
+ notebookDiff: null,
598
+ serverPath,
599
+ isBinary: false,
600
+ error: null
601
+ });
508
602
  const { oldRef, newRef } = resolveReferences(change);
509
- const serverPath =
510
- repoPath.length > 0 ? PathExt.join(repoPath, change.path) : change.path;
511
603
  void (async () => {
512
604
  try {
513
605
  if (change.isBinary === true) {
@@ -861,6 +953,10 @@ function NotebookViewModeControl(props: {
861
953
  content.notebookViewModeChanged.disconnect(handler);
862
954
  };
863
955
  }, [content]);
956
+ if (!content.hasNotebookView) {
957
+ return <></>;
958
+ }
959
+
864
960
  return (
865
961
  <div
866
962
  className="jp-xtralab-DiffWidget-segmented"
package/src/git/index.ts CHANGED
@@ -16,7 +16,11 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
16
16
  import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
17
17
 
18
18
  import { CommandArguments, CommandIDs, registerGitCommands } from './commands';
19
- import { DiffContentWidget, diffWidgetId } from './diffWidget';
19
+ import {
20
+ DiffContentWidget,
21
+ PREVIEW_DIFF_WIDGET_ID,
22
+ pinnedDiffWidgetId
23
+ } from './diffWidget';
20
24
  import type { MainAreaWidget } from '@jupyterlab/apputils';
21
25
  import { GitPanel } from './panel';
22
26
  import { IFileChange } from './tokens';
@@ -62,15 +66,22 @@ const plugin: JupyterFrontEndPlugin<void> = {
62
66
  });
63
67
 
64
68
  const findDiff = (
65
- change: IFileChange
69
+ change: IFileChange,
70
+ pin = false
66
71
  ): MainAreaWidget<DiffContentWidget> | undefined => {
67
- const id = diffWidgetId(change);
68
- return tracker.find(widget => widget.id === id) ?? undefined;
72
+ const id = pin ? pinnedDiffWidgetId(change) : PREVIEW_DIFF_WIDGET_ID;
73
+ const existing = tracker.find(
74
+ widget => !widget.isDisposed && widget.id === id
75
+ );
76
+ return existing ?? undefined;
69
77
  };
70
78
 
71
79
  const panel = new GitPanel({
72
- openDiff: change => {
80
+ openDiff: (change, options) => {
73
81
  const args: CommandArguments.IOpenDiff = { repoPath, change };
82
+ if (options?.pin === true) {
83
+ args.pin = true;
84
+ }
74
85
  void app.commands.execute(
75
86
  CommandIDs.openDiff,
76
87
  args as unknown as ReadonlyPartialJSONObject
@@ -87,7 +98,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
87
98
  showError: showErrorMessage
88
99
  });
89
100
 
90
- app.shell.add(panel, 'left', { rank: 800 });
101
+ app.shell.add(panel, 'left', { rank: 60 });
91
102
  if (restorer !== null) {
92
103
  restorer.add(panel, panel.id);
93
104
  }
@@ -101,6 +112,21 @@ const plugin: JupyterFrontEndPlugin<void> = {
101
112
  rendermime,
102
113
  onChanged: () => panel.refresh(),
103
114
  trackDiff: widget => tracker.add(widget),
115
+ onPinned: current => {
116
+ const existing = findDiff(current.content.change, true);
117
+ if (
118
+ existing !== undefined &&
119
+ existing !== current &&
120
+ !existing.isDisposed
121
+ ) {
122
+ app.shell.activateById(existing.id);
123
+ current.close();
124
+ return;
125
+ }
126
+ current.id = pinnedDiffWidgetId(current.content.change);
127
+ current.title.className = '';
128
+ app.shell.activateById(current.id);
129
+ },
104
130
  findDiff
105
131
  });
106
132
  }
@@ -21,7 +21,12 @@ import { FileChangeStatus, IFileChange, IGitStatusResult } from './tokens';
21
21
  */
22
22
  export interface IGitPanelHandlers {
23
23
  /** Open a diff viewer for the given file in the main area. */
24
- openDiff(change: IFileChange): void;
24
+ openDiff(
25
+ change: IFileChange,
26
+ options?: {
27
+ pin?: boolean;
28
+ }
29
+ ): void;
25
30
  /** Show a confirmation dialog. Returns true when the user accepted. */
26
31
  confirm(title: string, body: string, accept: string): Promise<boolean>;
27
32
  /** Show an error dialog. */
@@ -281,7 +286,11 @@ export function GitPanelComponent(props: {
281
286
  key={`${change.group}:${change.path}`}
282
287
  change={change}
283
288
  pendingAction={pendingActions[`${change.group}:${change.path}`]}
284
- onClick={() => handlers.openDiff(change)}
289
+ onClick={event =>
290
+ handlers.openDiff(change, {
291
+ pin: event.ctrlKey || event.metaKey
292
+ })
293
+ }
285
294
  actions={[
286
295
  {
287
296
  label: 'Unstage changes',
@@ -314,7 +323,11 @@ export function GitPanelComponent(props: {
314
323
  key={`${change.group}:${change.path}`}
315
324
  change={change}
316
325
  pendingAction={pendingActions[`${change.group}:${change.path}`]}
317
- onClick={() => handlers.openDiff(change)}
326
+ onClick={event =>
327
+ handlers.openDiff(change, {
328
+ pin: event.ctrlKey || event.metaKey
329
+ })
330
+ }
318
331
  actions={[
319
332
  {
320
333
  label: 'Discard changes',
@@ -365,7 +378,6 @@ function PanelHeader(props: {
365
378
  return (
366
379
  <div className="jp-xtralab-GitPanel-header">
367
380
  <div className="jp-xtralab-GitPanel-headerInfo">
368
- <div className="jp-xtralab-GitPanel-title">Source Control</div>
369
381
  {repoName !== null ? (
370
382
  <div className="jp-xtralab-GitPanel-repo" title={topLevel ?? ''}>
371
383
  {repoName}
@@ -442,7 +454,7 @@ function ChangeSection(props: {
442
454
  function ChangeRow(props: {
443
455
  change: IFileChange;
444
456
  pendingAction: string | undefined;
445
- onClick: () => void;
457
+ onClick: (event: React.MouseEvent<HTMLDivElement>) => void;
446
458
  actions: IRowAction[];
447
459
  }): React.ReactElement {
448
460
  const { change, pendingAction, onClick, actions } = props;
package/src/index.ts CHANGED
@@ -2,6 +2,10 @@ import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
2
 
3
3
  import fileBrowserPlugin from './fileBrowser';
4
4
  import gitPlugin from './git';
5
+ import launcherPlugin from './launcher';
6
+ import menusPlugin from './menus';
7
+ import sidebarPlugin from './sidebar';
8
+ import statusBarPlugin from './statusBar';
5
9
 
6
10
  /**
7
11
  * Every plugin contributed by `xtralab`. The entry point of the
@@ -12,7 +16,11 @@ import gitPlugin from './git';
12
16
  */
13
17
  const plugins: JupyterFrontEndPlugin<unknown>[] = [
14
18
  fileBrowserPlugin,
15
- gitPlugin
19
+ gitPlugin,
20
+ launcherPlugin,
21
+ menusPlugin,
22
+ sidebarPlugin,
23
+ statusBarPlugin
16
24
  ];
17
25
 
18
26
  export default plugins;